From a97bf8bc96b345ec9a8c0d195447f93540f8208f Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Thu, 13 Mar 2025 14:43:07 +0700 Subject: [PATCH 01/26] test(pytest-bdd): cover attachments with tests --- .../acceptance/attachments_test.py | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 tests/allure_pytest_bdd/acceptance/attachments_test.py diff --git a/tests/allure_pytest_bdd/acceptance/attachments_test.py b/tests/allure_pytest_bdd/acceptance/attachments_test.py new file mode 100644 index 00000000..357dcd01 --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/attachments_test.py @@ -0,0 +1,180 @@ +from hamcrest import assert_that +from hamcrest import equal_to, ends_with + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_attachment_with_content, has_step + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_attach_content_from_scenario_function(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.attach("Lorem Ipsum", name="foo") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_attachment_with_content( + allure_results.attachments, + equal_to("Lorem Ipsum"), + name="foo", + ) + ) + ) + + +def test_attach_file_from_scenario_function(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.attach.file(__file__, name="foo") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_attachment_with_content( + allure_results.attachments, + ends_with("test_attach_file_from_scenario_function.py"), + name="foo", + ) + ) + ) + + +def test_attach_content_from_step(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + When data is attached + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, when + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @when("data is attached") + def when_data_is_attached(): + allure.attach("Lorem Ipsum", name="foo") + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "When data is attached", + has_attachment_with_content( + allure_results.attachments, + equal_to("Lorem Ipsum"), + name="foo", + ), + ), + ), + ) + + +def test_attach_file_from_step(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + When a file is attached + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, when + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @when("a file is attached") + def when_file_is_attached(): + allure.attach.file(__file__, name="foo") + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "When a file is attached", + has_attachment_with_content( + allure_results.attachments, + ends_with("test_attach_file_from_step.py"), + name="foo", + ), + ), + ), + ) From 3ab77449f79b6e28f3f798764ebcf9bdacf1636b Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Thu, 13 Mar 2025 16:32:32 +0700 Subject: [PATCH 02/26] feat(pytest-bdd): add description support --- allure-pytest-bdd/src/allure_api.py | 20 ++ allure-pytest-bdd/src/plugin.py | 20 +- allure-pytest-bdd/src/pytest_bdd_listener.py | 17 +- allure-pytest-bdd/src/utils.py | 32 +++ .../acceptance/description_test.py | 227 ++++++++++++++++++ 5 files changed, 309 insertions(+), 7 deletions(-) create mode 100644 allure-pytest-bdd/src/allure_api.py create mode 100644 tests/allure_pytest_bdd/acceptance/description_test.py diff --git a/allure-pytest-bdd/src/allure_api.py b/allure-pytest-bdd/src/allure_api.py new file mode 100644 index 00000000..b4a2cfda --- /dev/null +++ b/allure-pytest-bdd/src/allure_api.py @@ -0,0 +1,20 @@ +import pytest + +import allure_commons + +from .utils import ALLURE_DESCRIPTION_MARK + + +class AllurePytestBddApi: + def __init__(self, lifecycle): + self.lifecycle = lifecycle + + @allure_commons.hookimpl + def decorate_as_description(self, test_description): + allure_description_mark = getattr(pytest.mark, ALLURE_DESCRIPTION_MARK) + return allure_description_mark(test_description) + + @allure_commons.hookimpl + def add_description(self, test_description): + with self.lifecycle.update_test_case() as test_result: + test_result.description = test_description diff --git a/allure-pytest-bdd/src/plugin.py b/allure-pytest-bdd/src/plugin.py index 5d6b8310..aa9a6d0c 100644 --- a/allure-pytest-bdd/src/plugin.py +++ b/allure-pytest-bdd/src/plugin.py @@ -1,7 +1,13 @@ import allure_commons import os from allure_commons.logger import AllureFileLogger +from allure_commons.lifecycle import AllureLifecycle + from .pytest_bdd_listener import PytestBDDListener +from .utils import ( + ALLURE_DESCRIPTION_MARK, +) +from .allure_api import AllurePytestBddApi def pytest_addoption(parser): @@ -25,18 +31,30 @@ def clean_up(): return clean_up +def register_marks(config): + config.addinivalue_line("markers", f"{ALLURE_DESCRIPTION_MARK}: allure description") + + def pytest_configure(config): + register_marks(config) + report_dir = config.option.allure_report_dir clean = False if config.option.collectonly else config.option.clean_alluredir if report_dir: report_dir = os.path.abspath(report_dir) - pytest_bdd_listener = PytestBDDListener() + lifecycle = AllureLifecycle() + + pytest_bdd_listener = PytestBDDListener(lifecycle) config.pluginmanager.register(pytest_bdd_listener) allure_commons.plugin_manager.register(pytest_bdd_listener) config.add_cleanup(cleanup_factory(pytest_bdd_listener)) + allure_api_impl = AllurePytestBddApi(lifecycle) + allure_commons.plugin_manager.register(allure_api_impl) + config.add_cleanup(cleanup_factory(allure_api_impl)) + file_logger = AllureFileLogger(report_dir, clean) allure_commons.plugin_manager.register(file_logger) config.add_cleanup(cleanup_factory(file_logger)) diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index d4c73115..bf55afbe 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -1,27 +1,31 @@ import pytest + import allure_commons from allure_commons.utils import now from allure_commons.utils import uuid4 from allure_commons.model2 import Label from allure_commons.model2 import Status - +from allure_commons.model2 import StatusDetails from allure_commons.types import LabelType, AttachmentType from allure_commons.utils import platform_label from allure_commons.utils import host_tag, thread_tag from allure_commons.utils import md5 + from .utils import get_uuid from .utils import get_step_name from .utils import get_status_details from .utils import get_pytest_report_status -from allure_commons.model2 import StatusDetails +from .utils import get_full_name +from .utils import get_name +from .utils import get_params +from .utils import get_allure_description + from functools import partial -from allure_commons.lifecycle import AllureLifecycle -from .utils import get_full_name, get_name, get_params class PytestBDDListener: - def __init__(self): - self.lifecycle = AllureLifecycle() + def __init__(self, lifecycle): + self.lifecycle = lifecycle self.host = host_tag() self.thread = thread_tag() @@ -41,6 +45,7 @@ def pytest_bdd_before_scenario(self, request, feature, scenario): with self.lifecycle.schedule_test_case(uuid=uuid) as test_result: test_result.fullName = full_name test_result.name = name + test_result.description = get_allure_description(request.node, feature, scenario) test_result.start = now() test_result.historyId = md5(request.node.nodeid) test_result.labels.append(Label(name=LabelType.HOST, value=self.host)) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index ac70aac2..1b4724ab 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -7,6 +7,38 @@ from allure_commons.utils import format_exception +ALLURE_DESCRIPTION_MARK = "allure_description" + + +def get_marker_value(item, keyword): + marker = item.get_closest_marker(keyword) + return marker.args[0] if marker and marker.args else None + + +def get_allure_description(item, feature, scenario): + value = get_marker_value(item, ALLURE_DESCRIPTION_MARK) + if value: + return value + + feature_description = resolve_description(feature.description) + scenario_description = resolve_description(scenario.description) + return "\n\n".join(filter(None, [feature_description, scenario_description])) + + +def resolve_description(description): + if isinstance(description, str): + return description + + if not isinstance(description, list): + return None + + while description and description[0] == "": + description = description[1:] + while description and description[-1] == "": + description = description[:-1] + return "\n".join(description) or None + + def get_step_name(step): return f"{step.keyword} {step.name}" diff --git a/tests/allure_pytest_bdd/acceptance/description_test.py b/tests/allure_pytest_bdd/acceptance/description_test.py new file mode 100644 index 00000000..4b0fb0ca --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/description_test.py @@ -0,0 +1,227 @@ +from hamcrest import assert_that +from hamcrest import equal_to + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_description +from allure_commons_test.result import has_description_html + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_description_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + + This will be overwritten by code + + Scenario: Bar + + This will be overwritten by code + + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.description("Lorem Ipsum") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_description( + equal_to("Lorem Ipsum"), + ) + ) + ) + + +def test_dynamic_description(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + + This will be overwritten by code + + Scenario: Bar + + This will be overwritten by code + + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.description("This will be overwritten by the runtime API") + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.description("Lorem Ipsum") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_description( + equal_to("Lorem Ipsum"), + ) + ) + ) + + +def test_scenario_description(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + + Lorem Ipsum + + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_description( + equal_to("Lorem Ipsum"), + ) + ) + ) + + +def test_feature_description(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + + Lorem Ipsum + + Scenario: Bar + + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_description( + equal_to("Lorem Ipsum"), + ) + ) + ) + + +def test_feature_and_scenario_description(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + + Lorem Ipsum + + Scenario: Bar + + Dolor Sit Amet + + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_description( + equal_to("Lorem Ipsum\n\nDolor Sit Amet"), + ) + ) + ) From 5d0503586a81bf5289ac64c77f49df0c22baa0b3 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Thu, 13 Mar 2025 16:58:47 +0700 Subject: [PATCH 03/26] feat(pytest-bdd): add description_html support --- allure-pytest-bdd/src/allure_api.py | 11 +++ allure-pytest-bdd/src/plugin.py | 8 +- allure-pytest-bdd/src/pytest_bdd_listener.py | 15 ++-- allure-pytest-bdd/src/utils.py | 5 ++ .../acceptance/description_test.py | 80 +++++++++++++++++++ 5 files changed, 109 insertions(+), 10 deletions(-) diff --git a/allure-pytest-bdd/src/allure_api.py b/allure-pytest-bdd/src/allure_api.py index b4a2cfda..1d6f3845 100644 --- a/allure-pytest-bdd/src/allure_api.py +++ b/allure-pytest-bdd/src/allure_api.py @@ -3,6 +3,7 @@ import allure_commons from .utils import ALLURE_DESCRIPTION_MARK +from .utils import ALLURE_DESCRIPTION_HTML_MARK class AllurePytestBddApi: @@ -18,3 +19,13 @@ def decorate_as_description(self, test_description): def add_description(self, test_description): with self.lifecycle.update_test_case() as test_result: test_result.description = test_description + + @allure_commons.hookimpl + def decorate_as_description_html(self, test_description_html): + allure_description_html_mark = getattr(pytest.mark, ALLURE_DESCRIPTION_HTML_MARK) + return allure_description_html_mark(test_description_html) + + @allure_commons.hookimpl + def add_description_html(self, test_description_html): + with self.lifecycle.update_test_case() as test_result: + test_result.descriptionHtml = test_description_html diff --git a/allure-pytest-bdd/src/plugin.py b/allure-pytest-bdd/src/plugin.py index aa9a6d0c..e6330341 100644 --- a/allure-pytest-bdd/src/plugin.py +++ b/allure-pytest-bdd/src/plugin.py @@ -3,11 +3,10 @@ from allure_commons.logger import AllureFileLogger from allure_commons.lifecycle import AllureLifecycle -from .pytest_bdd_listener import PytestBDDListener -from .utils import ( - ALLURE_DESCRIPTION_MARK, -) from .allure_api import AllurePytestBddApi +from .pytest_bdd_listener import PytestBDDListener +from .utils import ALLURE_DESCRIPTION_MARK +from .utils import ALLURE_DESCRIPTION_HTML_MARK def pytest_addoption(parser): @@ -33,6 +32,7 @@ def clean_up(): def register_marks(config): config.addinivalue_line("markers", f"{ALLURE_DESCRIPTION_MARK}: allure description") + config.addinivalue_line("markers", f"{ALLURE_DESCRIPTION_HTML_MARK}: allure description in HTML") def pytest_configure(config): diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index bf55afbe..ed8e1175 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -19,6 +19,7 @@ from .utils import get_name from .utils import get_params from .utils import get_allure_description +from .utils import get_allure_description_html from functools import partial @@ -39,24 +40,26 @@ def _scenario_finalizer(self, scenario): @pytest.hookimpl def pytest_bdd_before_scenario(self, request, feature, scenario): - uuid = get_uuid(request.node.nodeid) + item = request.node + uuid = get_uuid(item.nodeid) full_name = get_full_name(feature, scenario) - name = get_name(request.node, scenario) + name = get_name(item, scenario) with self.lifecycle.schedule_test_case(uuid=uuid) as test_result: test_result.fullName = full_name test_result.name = name - test_result.description = get_allure_description(request.node, feature, scenario) + test_result.description = get_allure_description(item, feature, scenario) + test_result.descriptionHtml = get_allure_description_html(item) test_result.start = now() - test_result.historyId = md5(request.node.nodeid) + test_result.historyId = md5(item.nodeid) test_result.labels.append(Label(name=LabelType.HOST, value=self.host)) test_result.labels.append(Label(name=LabelType.THREAD, value=self.thread)) test_result.labels.append(Label(name=LabelType.FRAMEWORK, value="pytest-bdd")) test_result.labels.append(Label(name=LabelType.LANGUAGE, value=platform_label())) test_result.labels.append(Label(name=LabelType.FEATURE, value=feature.name)) - test_result.parameters = get_params(request.node) + test_result.parameters = get_params(item) finalizer = partial(self._scenario_finalizer, scenario) - request.node.addfinalizer(finalizer) + item.addfinalizer(finalizer) @pytest.hookimpl def pytest_bdd_after_scenario(self, request, feature, scenario): diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index 1b4724ab..3aa187fc 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -8,6 +8,7 @@ ALLURE_DESCRIPTION_MARK = "allure_description" +ALLURE_DESCRIPTION_HTML_MARK = "allure_description_html" def get_marker_value(item, keyword): @@ -25,6 +26,10 @@ def get_allure_description(item, feature, scenario): return "\n\n".join(filter(None, [feature_description, scenario_description])) +def get_allure_description_html(item): + return get_marker_value(item, ALLURE_DESCRIPTION_HTML_MARK) + + def resolve_description(description): if isinstance(description, str): return description diff --git a/tests/allure_pytest_bdd/acceptance/description_test.py b/tests/allure_pytest_bdd/acceptance/description_test.py index 4b0fb0ca..049305c4 100644 --- a/tests/allure_pytest_bdd/acceptance/description_test.py +++ b/tests/allure_pytest_bdd/acceptance/description_test.py @@ -54,6 +54,46 @@ def given_noop(): ) +def test_description_html_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.description_html("Lorem Ipsum") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_description_html( + equal_to("Lorem Ipsum"), + ) + ) + ) + + def test_dynamic_description(allure_pytest_bdd_runner: AllurePytestRunner): feature_content = ( """ @@ -100,6 +140,46 @@ def given_noop(): ) +def test_dynamic_description_html(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.description_html("This will be overwritten by the runtime API") + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.description_html("Lorem Ipsum") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_description_html( + equal_to("Lorem Ipsum"), + ) + ) + ) + + def test_scenario_description(allure_pytest_bdd_runner: AllurePytestRunner): feature_content = ( """ From 071bb39dc525e7e96a1b9a810b42a92dc98653a4 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Thu, 13 Mar 2025 18:00:52 +0700 Subject: [PATCH 04/26] feat(pytest-bdd): add title support --- allure-pytest-bdd/src/allure_api.py | 11 ++ allure-pytest-bdd/src/plugin.py | 2 + allure-pytest-bdd/src/pytest_bdd_listener.py | 14 +- allure-pytest-bdd/src/utils.py | 34 ++++- .../acceptance/title_test.py | 134 ++++++++++++++++++ 5 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 tests/allure_pytest_bdd/acceptance/title_test.py diff --git a/allure-pytest-bdd/src/allure_api.py b/allure-pytest-bdd/src/allure_api.py index 1d6f3845..3b4b11d6 100644 --- a/allure-pytest-bdd/src/allure_api.py +++ b/allure-pytest-bdd/src/allure_api.py @@ -2,6 +2,7 @@ import allure_commons +from .utils import ALLURE_TITLE_MARK from .utils import ALLURE_DESCRIPTION_MARK from .utils import ALLURE_DESCRIPTION_HTML_MARK @@ -10,6 +11,16 @@ class AllurePytestBddApi: def __init__(self, lifecycle): self.lifecycle = lifecycle + @allure_commons.hookimpl + def decorate_as_title(self, test_title): + allure_title_mark = getattr(pytest.mark, ALLURE_TITLE_MARK) + return allure_title_mark(test_title) + + @allure_commons.hookimpl + def add_title(self, test_title): + with self.lifecycle.update_test_case() as test_result: + test_result.name = test_title + @allure_commons.hookimpl def decorate_as_description(self, test_description): allure_description_mark = getattr(pytest.mark, ALLURE_DESCRIPTION_MARK) diff --git a/allure-pytest-bdd/src/plugin.py b/allure-pytest-bdd/src/plugin.py index e6330341..93669dd7 100644 --- a/allure-pytest-bdd/src/plugin.py +++ b/allure-pytest-bdd/src/plugin.py @@ -5,6 +5,7 @@ from .allure_api import AllurePytestBddApi from .pytest_bdd_listener import PytestBDDListener +from .utils import ALLURE_TITLE_MARK from .utils import ALLURE_DESCRIPTION_MARK from .utils import ALLURE_DESCRIPTION_HTML_MARK @@ -31,6 +32,7 @@ def clean_up(): def register_marks(config): + config.addinivalue_line("markers", f"{ALLURE_TITLE_MARK}: allure title marker") config.addinivalue_line("markers", f"{ALLURE_DESCRIPTION_MARK}: allure description") config.addinivalue_line("markers", f"{ALLURE_DESCRIPTION_HTML_MARK}: allure description in HTML") diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index ed8e1175..a4c2deab 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -16,8 +16,9 @@ from .utils import get_status_details from .utils import get_pytest_report_status from .utils import get_full_name -from .utils import get_name -from .utils import get_params +from .utils import get_test_name +from .utils import get_pytest_params +from .utils import convert_params from .utils import get_allure_description from .utils import get_allure_description_html @@ -42,11 +43,10 @@ def _scenario_finalizer(self, scenario): def pytest_bdd_before_scenario(self, request, feature, scenario): item = request.node uuid = get_uuid(item.nodeid) - full_name = get_full_name(feature, scenario) - name = get_name(item, scenario) + params = get_pytest_params(item) with self.lifecycle.schedule_test_case(uuid=uuid) as test_result: - test_result.fullName = full_name - test_result.name = name + test_result.fullName = get_full_name(feature, scenario) + 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) test_result.start = now() @@ -56,7 +56,7 @@ def pytest_bdd_before_scenario(self, request, feature, scenario): test_result.labels.append(Label(name=LabelType.FRAMEWORK, value="pytest-bdd")) test_result.labels.append(Label(name=LabelType.LANGUAGE, value=platform_label())) test_result.labels.append(Label(name=LabelType.FEATURE, value=feature.name)) - test_result.parameters = get_params(item) + test_result.parameters = convert_params(params) finalizer = partial(self._scenario_finalizer, scenario) item.addfinalizer(finalizer) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index 3aa187fc..644d9ef3 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -1,6 +1,7 @@ import os from uuid import UUID from allure_commons.utils import md5 +from allure_commons.utils import SafeFormatter from allure_commons.model2 import StatusDetails from allure_commons.model2 import Status from allure_commons.model2 import Parameter @@ -9,6 +10,7 @@ ALLURE_DESCRIPTION_MARK = "allure_description" ALLURE_DESCRIPTION_HTML_MARK = "allure_description_html" +ALLURE_TITLE_MARK = "allure_title" def get_marker_value(item, keyword): @@ -16,6 +18,14 @@ def get_marker_value(item, keyword): return marker.args[0] if marker and marker.args else None +def get_allure_title(item): + return get_marker_value(item, ALLURE_TITLE_MARK) + + +def interpolate_args(format_str, args): + return SafeFormatter().format(format_str, **args) if args else format_str + + def get_allure_description(item, feature, scenario): value = get_marker_value(item, ALLURE_DESCRIPTION_MARK) if value: @@ -48,7 +58,11 @@ def get_step_name(step): return f"{step.keyword} {step.name}" -def get_name(node, scenario): +def get_test_name(node, scenario, params): + allure_name = get_allure_title(node) + if allure_name: + return interpolate_args(allure_name, params) + if hasattr(node, 'callspec'): parts = node.nodeid.rsplit("[") params = parts[-1] @@ -79,9 +93,17 @@ def get_pytest_report_status(pytest_report): return status -def get_params(node): +def get_pytest_params(node): if hasattr(node, 'callspec'): - params = dict(node.callspec.params) - outline_params = params.pop('_pytest_bdd_example', {}) - params.update(outline_params) - return [Parameter(name=name, value=value) for name, value in params.items()] + pytest_params = dict(node.callspec.params) + pytest_bdd_params = pytest_params.pop('_pytest_bdd_example', {}) + return {**pytest_bdd_params, **pytest_params} + + +def convert_params(pytest_params): + return [ + Parameter( + name=name, + value=value, + ) for name, value in (pytest_params or {}).items() + ] diff --git a/tests/allure_pytest_bdd/acceptance/title_test.py b/tests/allure_pytest_bdd/acceptance/title_test.py new file mode 100644 index 00000000..f9671845 --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/title_test.py @@ -0,0 +1,134 @@ +from hamcrest import assert_that +from hamcrest import equal_to + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_title + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_title_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.title("Lorem Ipsum") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_title( + equal_to("Lorem Ipsum"), + ) + ) + ) + + +def test_title_interpolations(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario Outline: Bar + Given noop + + Examples: + | bar | + | Ipsum | + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @allure.title("{foo} {bar}") + @pytest.mark.parametrize("foo", ["Lorem"]) + @scenario("sample.feature", "Bar") + def test_scenario(foo): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_title( + equal_to("Lorem Ipsum"), + ) + ) + ) + + +def test_dynamic_title(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @allure.title("This will be overwritten") + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.title("Lorem Ipsum") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_title( + equal_to("Lorem Ipsum"), + ) + ) + ) From 0b04c9895119f2a2b8e7e13a638e078f9caa8cfc Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 14 Mar 2025 18:45:09 +0700 Subject: [PATCH 05/26] feat(pytest-bdd): add labels support --- allure-pytest-bdd/src/allure_api.py | 12 ++ allure-pytest-bdd/src/plugin.py | 2 + allure-pytest-bdd/src/pytest_bdd_listener.py | 2 + allure-pytest-bdd/src/utils.py | 21 +++ .../acceptance/labels/__init__.py | 0 .../acceptance/labels/labels_test.py | 143 ++++++++++++++++++ 6 files changed, 180 insertions(+) create mode 100644 tests/allure_pytest_bdd/acceptance/labels/__init__.py create mode 100644 tests/allure_pytest_bdd/acceptance/labels/labels_test.py diff --git a/allure-pytest-bdd/src/allure_api.py b/allure-pytest-bdd/src/allure_api.py index 3b4b11d6..6e8d2828 100644 --- a/allure-pytest-bdd/src/allure_api.py +++ b/allure-pytest-bdd/src/allure_api.py @@ -1,10 +1,12 @@ import pytest import allure_commons +from allure_commons.model2 import Label from .utils import ALLURE_TITLE_MARK from .utils import ALLURE_DESCRIPTION_MARK from .utils import ALLURE_DESCRIPTION_HTML_MARK +from .utils import ALLURE_LABEL_MARK class AllurePytestBddApi: @@ -40,3 +42,13 @@ def decorate_as_description_html(self, test_description_html): def add_description_html(self, test_description_html): with self.lifecycle.update_test_case() as test_result: test_result.descriptionHtml = test_description_html + + @allure_commons.hookimpl + def decorate_as_label(self, label_type, labels): + allure_label_mark = getattr(pytest.mark, ALLURE_LABEL_MARK) + return allure_label_mark(*labels, label_type=label_type) + + @allure_commons.hookimpl + def add_label(self, label_type, labels): + with self.lifecycle.update_test_case() as test_result: + test_result.labels.extend(Label(name=label_type, value=value) for value in labels or []) diff --git a/allure-pytest-bdd/src/plugin.py b/allure-pytest-bdd/src/plugin.py index 93669dd7..5a5c0615 100644 --- a/allure-pytest-bdd/src/plugin.py +++ b/allure-pytest-bdd/src/plugin.py @@ -8,6 +8,7 @@ from .utils import ALLURE_TITLE_MARK from .utils import ALLURE_DESCRIPTION_MARK from .utils import ALLURE_DESCRIPTION_HTML_MARK +from .utils import ALLURE_LABEL_MARK def pytest_addoption(parser): @@ -35,6 +36,7 @@ def register_marks(config): config.addinivalue_line("markers", f"{ALLURE_TITLE_MARK}: allure title marker") config.addinivalue_line("markers", f"{ALLURE_DESCRIPTION_MARK}: allure description") config.addinivalue_line("markers", f"{ALLURE_DESCRIPTION_HTML_MARK}: allure description in HTML") + config.addinivalue_line("markers", f"{ALLURE_LABEL_MARK}: allure label marker") def pytest_configure(config): diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index a4c2deab..8de2d2e5 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -19,6 +19,7 @@ from .utils import get_test_name from .utils import get_pytest_params from .utils import convert_params +from .utils import get_allure_labels from .utils import get_allure_description from .utils import get_allure_description_html @@ -56,6 +57,7 @@ def pytest_bdd_before_scenario(self, request, feature, scenario): test_result.labels.append(Label(name=LabelType.FRAMEWORK, value="pytest-bdd")) test_result.labels.append(Label(name=LabelType.LANGUAGE, value=platform_label())) test_result.labels.append(Label(name=LabelType.FEATURE, value=feature.name)) + test_result.labels.extend(get_allure_labels(item)) test_result.parameters = convert_params(params) finalizer = partial(self._scenario_finalizer, scenario) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index 644d9ef3..5c4636cd 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -2,6 +2,7 @@ from uuid import UUID from allure_commons.utils import md5 from allure_commons.utils import SafeFormatter +from allure_commons.model2 import Label from allure_commons.model2 import StatusDetails from allure_commons.model2 import Status from allure_commons.model2 import Parameter @@ -11,6 +12,7 @@ ALLURE_DESCRIPTION_MARK = "allure_description" ALLURE_DESCRIPTION_HTML_MARK = "allure_description_html" ALLURE_TITLE_MARK = "allure_title" +ALLURE_LABEL_MARK = 'allure_label' def get_marker_value(item, keyword): @@ -40,6 +42,25 @@ def get_allure_description_html(item): return get_marker_value(item, ALLURE_DESCRIPTION_HTML_MARK) +def iter_all_labels(item): + for mark in item.iter_markers(name=ALLURE_LABEL_MARK): + name = mark.kwargs.get("label_type") + if name: + yield from ((name, value) for value in mark.args or []) + + +def iter_label_values(item, name): + return (pair for pair in iter_all_labels(item) if pair[0] == name) + + +def convert_labels(labels): + return [Label(name, value) for name, value in labels] + + +def get_allure_labels(item): + return convert_labels(iter_all_labels(item)) + + def resolve_description(description): if isinstance(description, str): return description diff --git a/tests/allure_pytest_bdd/acceptance/labels/__init__.py b/tests/allure_pytest_bdd/acceptance/labels/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/allure_pytest_bdd/acceptance/labels/labels_test.py b/tests/allure_pytest_bdd/acceptance/labels/labels_test.py new file mode 100644 index 00000000..40e13d02 --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/labels/labels_test.py @@ -0,0 +1,143 @@ +from hamcrest import assert_that +from hamcrest import equal_to +from hamcrest import all_of +from hamcrest import has_entry +from hamcrest import contains_inanyorder + +from allure_commons_test.report import has_test_case +from allure_commons_test.label import has_label + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_default_labels(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_entry( + "labels", + contains_inanyorder( + has_entry("name", "host"), + has_entry("name", "thread"), + all_of( + has_entry("name", "framework"), + has_entry("value", "pytest-bdd"), + ), + has_entry("name", "language"), + all_of( + has_entry("name", "feature"), + has_entry("value", "Foo"), + ), + ), + ), + ) + ) + + +def test_label_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.label("foo", "bar", "baz") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + all_of( + has_label("foo", equal_to("bar")), + has_label("foo", equal_to("baz")), + ), + + ) + ) + + +def test_dynamic_label(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.label("foo", "bar", "baz") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + all_of( + has_label("foo", equal_to("bar")), + has_label("foo", equal_to("baz")), + ) + ) + ) From 4748a3ddaa0560a29126cc7caedae594cd65abfc Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 14 Mar 2025 19:53:49 +0700 Subject: [PATCH 06/26] fix(pytest-bdd): unduplicate feature labels --- allure-pytest-bdd/src/pytest_bdd_listener.py | 5 +- allure-pytest-bdd/src/utils.py | 20 +++++ .../acceptance/labels/features_test.py | 90 +++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 tests/allure_pytest_bdd/acceptance/labels/features_test.py diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index 8de2d2e5..b4d11981 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -11,6 +11,8 @@ from allure_commons.utils import host_tag, thread_tag from allure_commons.utils import md5 +from .utils import set_feature_and_scenario +from .utils import apply_defaults from .utils import get_uuid from .utils import get_step_name from .utils import get_status_details @@ -43,6 +45,7 @@ def _scenario_finalizer(self, scenario): @pytest.hookimpl def pytest_bdd_before_scenario(self, request, feature, scenario): item = request.node + set_feature_and_scenario(item, feature, scenario) uuid = get_uuid(item.nodeid) params = get_pytest_params(item) with self.lifecycle.schedule_test_case(uuid=uuid) as test_result: @@ -56,7 +59,6 @@ def pytest_bdd_before_scenario(self, request, feature, scenario): test_result.labels.append(Label(name=LabelType.THREAD, value=self.thread)) test_result.labels.append(Label(name=LabelType.FRAMEWORK, value="pytest-bdd")) test_result.labels.append(Label(name=LabelType.LANGUAGE, value=platform_label())) - test_result.labels.append(Label(name=LabelType.FEATURE, value=feature.name)) test_result.labels.extend(get_allure_labels(item)) test_result.parameters = convert_params(params) @@ -130,6 +132,7 @@ def pytest_runtest_makereport(self, item, call): self.attach_data(report.capstdout, "stdout", AttachmentType.TEXT, None) if report.capstderr: self.attach_data(report.capstderr, "stderr", AttachmentType.TEXT, None) + apply_defaults(item, test_result) if report.when == 'teardown': self.lifecycle.write_test_case(uuid=uuid) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index 5c4636cd..f15cd3f7 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -1,4 +1,5 @@ import os +import pytest from uuid import UUID from allure_commons.utils import md5 from allure_commons.utils import SafeFormatter @@ -6,8 +7,10 @@ from allure_commons.model2 import StatusDetails from allure_commons.model2 import Status from allure_commons.model2 import Parameter +from allure_commons.types import LabelType from allure_commons.utils import format_exception +ALLURE_PYTEST_BDD_HASHKEY = pytest.StashKey() ALLURE_DESCRIPTION_MARK = "allure_description" ALLURE_DESCRIPTION_HTML_MARK = "allure_description_html" @@ -15,6 +18,14 @@ ALLURE_LABEL_MARK = 'allure_label' +def set_feature_and_scenario(item, feature, scenario): + item.stash[ALLURE_PYTEST_BDD_HASHKEY] = (feature, scenario) + + +def get_feature_and_scenario(item): + return item.stash.get(ALLURE_PYTEST_BDD_HASHKEY, (None, None)) + + def get_marker_value(item, keyword): marker = item.get_closest_marker(keyword) return marker.args[0] if marker and marker.args else None @@ -128,3 +139,12 @@ def convert_params(pytest_params): value=value, ) for name, value in (pytest_params or {}).items() ] + +def apply_defaults(item, test_result): + feature, _ = get_feature_and_scenario(item) + if feature is None: + return + + existing_labels = { label.name for label in test_result.labels } + if LabelType.FEATURE not in existing_labels: + test_result.labels.append(Label(name=LabelType.FEATURE, value=feature.name)) diff --git a/tests/allure_pytest_bdd/acceptance/labels/features_test.py b/tests/allure_pytest_bdd/acceptance/labels/features_test.py new file mode 100644 index 00000000..c8def23a --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/labels/features_test.py @@ -0,0 +1,90 @@ +from hamcrest import assert_that +from hamcrest import all_of +from hamcrest import not_ + +from allure_commons_test.report import has_test_case +from allure_commons_test.label import has_feature + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_feature_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.feature("Lorem Ipsum") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + all_of( + has_feature("Lorem Ipsum"), + not_(has_feature("Foo")), + ) + ) + ) + + +def test_dynamic_feature(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.feature("Lorem Ipsum") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + cli_args=["--capture=no"] + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + all_of( + has_feature("Lorem Ipsum"), + not_(has_feature("Foo")), + ) + ) + ) From b2777ddc03b4ffc196fb66d260cf61f819d32734 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 14 Mar 2025 21:50:40 +0700 Subject: [PATCH 07/26] feat(pytest-bdd): add tags support --- allure-pytest-bdd/src/pytest_bdd_listener.py | 4 +- allure-pytest-bdd/src/utils.py | 41 +- .../acceptance/labels/tags_test.py | 396 ++++++++++++++++++ 3 files changed, 434 insertions(+), 7 deletions(-) create mode 100644 tests/allure_pytest_bdd/acceptance/labels/tags_test.py diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index b4d11981..7a75d216 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -12,7 +12,7 @@ from allure_commons.utils import md5 from .utils import set_feature_and_scenario -from .utils import apply_defaults +from .utils import post_process_test_result from .utils import get_uuid from .utils import get_step_name from .utils import get_status_details @@ -132,7 +132,7 @@ def pytest_runtest_makereport(self, item, call): self.attach_data(report.capstdout, "stdout", AttachmentType.TEXT, None) if report.capstderr: self.attach_data(report.capstderr, "stderr", AttachmentType.TEXT, None) - apply_defaults(item, test_result) + post_process_test_result(item, test_result) if report.when == 'teardown': self.lifecycle.write_test_case(uuid=uuid) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index f15cd3f7..2fb99aa8 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -17,6 +17,15 @@ ALLURE_TITLE_MARK = "allure_title" ALLURE_LABEL_MARK = 'allure_label' +MARK_NAMES_TO_IGNORE = { + "usefixtures", + "filterwarnings", + "skip", + "skipif", + "xfail", + "parametrize", +} + def set_feature_and_scenario(item, feature, scenario): item.stash[ALLURE_PYTEST_BDD_HASHKEY] = (feature, scenario) @@ -60,6 +69,17 @@ def iter_all_labels(item): yield from ((name, value) for value in mark.args or []) +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 iter_pytest_tags(item: pytest.Function): + for mark in item.iter_markers(): + if should_convert_mark_to_tag(mark): + yield LabelType.TAG, mark.name + + def iter_label_values(item, name): return (pair for pair in iter_all_labels(item) if pair[0] == name) @@ -140,11 +160,22 @@ def convert_params(pytest_params): ) for name, value in (pytest_params or {}).items() ] -def apply_defaults(item, test_result): + +def iter_pytest_labels(item, test_result): feature, _ = get_feature_and_scenario(item) - if feature is None: - return - existing_labels = { label.name for label in test_result.labels } + existing_labels = {label.name for label in test_result.labels} + if LabelType.FEATURE not in existing_labels: - test_result.labels.append(Label(name=LabelType.FEATURE, value=feature.name)) + yield LabelType.FEATURE, feature.name + + yield from iter_pytest_tags(item) + + +def post_process_test_result(item, test_result): + test_result.labels.extend( + Label( + name=name, + value=value, + ) for name, value in iter_pytest_labels(item, test_result) + ) diff --git a/tests/allure_pytest_bdd/acceptance/labels/tags_test.py b/tests/allure_pytest_bdd/acceptance/labels/tags_test.py new file mode 100644 index 00000000..f54a9de2 --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/labels/tags_test.py @@ -0,0 +1,396 @@ +import pytest + +from hamcrest import assert_that +from hamcrest import not_ +from hamcrest import all_of + +from allure_commons_test.report import has_test_case +from allure_commons_test.label import has_tag + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_tag_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.tag("foo", "bar") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_tag("foo"), + has_tag("bar"), + ) + ) + + +def test_dynamic_tag(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.tag("foo", "bar") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_tag("foo"), + has_tag("bar"), + ) + ) + + +def test_pytest_mark_reported(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import pytest + + @pytest.mark.foo + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + conftest_content = ( + """ + def pytest_configure(config): + config.addinivalue_line("markers", f"foo: lorem ipsum") + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + conftest_literal=conftest_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_tag("foo"), + ) + ) + + +def test_pytest_marks_with_arg_not_reported(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import pytest + + @pytest.mark.foo("bar") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + conftest_content = ( + """ + def pytest_configure(config): + config.addinivalue_line("markers", f"foo: lorem ipsum") + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + conftest_literal=conftest_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + not_(has_tag("foo")), + ) + ) + + +def test_pytest_marks_with_kwarg_not_reported(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import pytest + + @pytest.mark.foo(foo="bar") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + conftest_content = ( + """ + def pytest_configure(config): + config.addinivalue_line("markers", f"foo: lorem ipsum") + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + conftest_literal=conftest_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + not_(has_tag("foo")), + ) + ) + + +# Can't check argless skip/skipif: skipepd tests currently not reported +@pytest.mark.parametrize("mark", ["usefixtures", "filterwarnings", "xfail"]) +def test_builtin_pytest_marks_not_reported(allure_pytest_bdd_runner: AllurePytestRunner, mark): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + f""" + from pytest_bdd import scenario, given + import pytest + + @pytest.mark.{mark} + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + not_(has_tag(mark)), + ) + ) + + +def test_parametrize_mark_not_reported(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import pytest + + @pytest.mark.parametrize("foo", ["bar"]) + @scenario("sample.feature", "Bar") + def test_scenario(foo): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + not_(has_tag("parametrize")), + ) + ) + + +def test_skipif_mark_not_reported(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import pytest + + @pytest.mark.skipif(False, reason="Lorem Ipsum") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + not_(has_tag("skipif")), + ) + ) + + +def test_gherkin_tags_reported(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + @foo + Feature: Foo + @bar + Scenario: Bar + Given noop + + @baz + Scenario: Baz + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenarios, given + import pytest + + scenarios("sample.feature") + + @given("noop") + def given_noop(): + pass + """ + ) + conftest_content = ( + """ + def pytest_configure(config): + config.addinivalue_line("markers", f"foo: foo") + config.addinivalue_line("markers", f"bar: bar") + config.addinivalue_line("markers", f"baz: baz") + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + conftest_literal=conftest_content, + ) + + assert_that( + allure_results, + all_of( + has_test_case( + "sample.feature:Bar", + has_tag("foo"), + has_tag("bar"), + ), + has_test_case( + "sample.feature:Baz", + has_tag("foo"), + has_tag("baz"), + ), + ), + ) From 4f92727d901eaa7d99255f5ffe5736fd06dcf9a8 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 14 Mar 2025 21:57:25 +0700 Subject: [PATCH 08/26] fix: classifier fixes --- allure-behave/setup.py | 3 +-- allure-nose2/setup.py | 2 +- allure-pytest-bdd/setup.py | 3 ++- allure-pytest/setup.py | 3 +-- allure-python-commons-test/setup.py | 2 +- allure-python-commons/setup.py | 2 +- allure-robotframework/setup.py | 2 +- 7 files changed, 8 insertions(+), 9 deletions(-) diff --git a/allure-behave/setup.py b/allure-behave/setup.py index 62cb593e..9bb3dc41 100644 --- a/allure-behave/setup.py +++ b/allure-behave/setup.py @@ -12,12 +12,12 @@ 'Topic :: Software Development :: Testing :: BDD', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ] setup_requires = [ @@ -66,4 +66,3 @@ def main(): if __name__ == '__main__': main() - diff --git a/allure-nose2/setup.py b/allure-nose2/setup.py index 2c64e8ff..6f7a1ec5 100644 --- a/allure-nose2/setup.py +++ b/allure-nose2/setup.py @@ -11,12 +11,12 @@ 'Topic :: Software Development :: Testing', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ] setup_requires = [ diff --git a/allure-pytest-bdd/setup.py b/allure-pytest-bdd/setup.py index 5f8cf7af..cdf802bc 100644 --- a/allure-pytest-bdd/setup.py +++ b/allure-pytest-bdd/setup.py @@ -10,14 +10,15 @@ 'License :: OSI Approved :: Apache Software License', 'Topic :: Software Development :: Quality Assurance', 'Topic :: Software Development :: Testing', + 'Topic :: Software Development :: Testing :: BDD', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ] setup_requires = [ diff --git a/allure-pytest/setup.py b/allure-pytest/setup.py index f88df22b..6597abf5 100644 --- a/allure-pytest/setup.py +++ b/allure-pytest/setup.py @@ -24,12 +24,12 @@ 'Topic :: Software Development :: Testing', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ] setup_requires = [ @@ -80,4 +80,3 @@ def main(): if __name__ == '__main__': main() - diff --git a/allure-python-commons-test/setup.py b/allure-python-commons-test/setup.py index 1f8d1aa1..bfcaddca 100644 --- a/allure-python-commons-test/setup.py +++ b/allure-python-commons-test/setup.py @@ -11,12 +11,12 @@ 'Topic :: Software Development :: Testing', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ] install_requires = [ diff --git a/allure-python-commons/setup.py b/allure-python-commons/setup.py index 91a1e1f0..ee645587 100644 --- a/allure-python-commons/setup.py +++ b/allure-python-commons/setup.py @@ -11,12 +11,12 @@ 'Topic :: Software Development :: Testing', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ] install_requires = [ diff --git a/allure-robotframework/setup.py b/allure-robotframework/setup.py index f333225c..8b194c29 100644 --- a/allure-robotframework/setup.py +++ b/allure-robotframework/setup.py @@ -13,12 +13,12 @@ 'Topic :: Software Development :: Testing', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ] setup_requires = [ From 3bda8ca1f1d73b611b89f1958bae2ca97f75310b Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 14 Mar 2025 23:12:04 +0700 Subject: [PATCH 09/26] test(pytest-bdd): cover specific labels with tests --- allure-python-commons-test/src/label.py | 8 ++ .../acceptance/labels/epics_test.py | 81 +++++++++++++++++ .../acceptance/labels/ids_test.py | 81 +++++++++++++++++ .../acceptance/labels/manuals_test.py | 81 +++++++++++++++++ .../acceptance/labels/parent_suites_test.py | 81 +++++++++++++++++ .../acceptance/labels/severities_test.py | 86 +++++++++++++++++++ .../acceptance/labels/stories_test.py | 81 +++++++++++++++++ .../acceptance/labels/sub_suites_test.py | 81 +++++++++++++++++ .../acceptance/labels/suites_test.py | 81 +++++++++++++++++ 9 files changed, 661 insertions(+) create mode 100644 tests/allure_pytest_bdd/acceptance/labels/epics_test.py create mode 100644 tests/allure_pytest_bdd/acceptance/labels/ids_test.py create mode 100644 tests/allure_pytest_bdd/acceptance/labels/manuals_test.py create mode 100644 tests/allure_pytest_bdd/acceptance/labels/parent_suites_test.py create mode 100644 tests/allure_pytest_bdd/acceptance/labels/severities_test.py create mode 100644 tests/allure_pytest_bdd/acceptance/labels/stories_test.py create mode 100644 tests/allure_pytest_bdd/acceptance/labels/sub_suites_test.py create mode 100644 tests/allure_pytest_bdd/acceptance/labels/suites_test.py diff --git a/allure-python-commons-test/src/label.py b/allure-python-commons-test/src/label.py index 12782b06..15d9e3d2 100644 --- a/allure-python-commons-test/src/label.py +++ b/allure-python-commons-test/src/label.py @@ -51,3 +51,11 @@ def has_parent_suite(parent_suite): def has_sub_suite(sub_suite): return has_label('subSuite', sub_suite) + + +def has_allure_id(allure_id): + return has_label('as_id', allure_id) + + +def has_manual(allure_id): + return has_label('ALLURE_MANUAL', allure_id) diff --git a/tests/allure_pytest_bdd/acceptance/labels/epics_test.py b/tests/allure_pytest_bdd/acceptance/labels/epics_test.py new file mode 100644 index 00000000..73df3477 --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/labels/epics_test.py @@ -0,0 +1,81 @@ +from hamcrest import assert_that + +from allure_commons_test.report import has_test_case +from allure_commons_test.label import has_epic + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_epic_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.epic("Lorem Ipsum") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_epic("Lorem Ipsum"), + ) + ) + + +def test_dynamic_epic(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.epic("Lorem Ipsum") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_epic("Lorem Ipsum"), + ) + ) diff --git a/tests/allure_pytest_bdd/acceptance/labels/ids_test.py b/tests/allure_pytest_bdd/acceptance/labels/ids_test.py new file mode 100644 index 00000000..685cca27 --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/labels/ids_test.py @@ -0,0 +1,81 @@ +from hamcrest import assert_that + +from allure_commons_test.report import has_test_case +from allure_commons_test.label import has_allure_id + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_id_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.id("1009") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_allure_id("1009"), + ) + ) + + +def test_dynamic_id(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.id("1009") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_allure_id("1009"), + ) + ) diff --git a/tests/allure_pytest_bdd/acceptance/labels/manuals_test.py b/tests/allure_pytest_bdd/acceptance/labels/manuals_test.py new file mode 100644 index 00000000..a33b1caa --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/labels/manuals_test.py @@ -0,0 +1,81 @@ +from hamcrest import assert_that + +from allure_commons_test.report import has_test_case +from allure_commons_test.label import has_manual + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_manual_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.manual + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_manual(True), + ) + ) + + +def test_dynamic_manual(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.manual() + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_manual(True), + ) + ) diff --git a/tests/allure_pytest_bdd/acceptance/labels/parent_suites_test.py b/tests/allure_pytest_bdd/acceptance/labels/parent_suites_test.py new file mode 100644 index 00000000..f0274542 --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/labels/parent_suites_test.py @@ -0,0 +1,81 @@ +from hamcrest import assert_that + +from allure_commons_test.report import has_test_case +from allure_commons_test.label import has_parent_suite + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_parent_suite_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.parent_suite("Lorem Ipsum") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_parent_suite("Lorem Ipsum"), + ) + ) + + +def test_dynamic_parent_suite(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.parent_suite("Lorem Ipsum") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_parent_suite("Lorem Ipsum"), + ) + ) diff --git a/tests/allure_pytest_bdd/acceptance/labels/severities_test.py b/tests/allure_pytest_bdd/acceptance/labels/severities_test.py new file mode 100644 index 00000000..3624df0b --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/labels/severities_test.py @@ -0,0 +1,86 @@ +import pytest +from hamcrest import assert_that + +import allure + +from allure_commons_test.report import has_test_case +from allure_commons_test.label import has_severity + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +@pytest.mark.parametrize("severity", allure.severity_level) +def test_severity_decorator(allure_pytest_bdd_runner: AllurePytestRunner, severity): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + f""" + from pytest_bdd import scenario, given + import allure + + @allure.severity(allure.severity_level.{severity.name}) + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_severity(severity.value), + ) + ) + + +@pytest.mark.parametrize("severity", allure.severity_level) +def test_dynamic_severity(allure_pytest_bdd_runner: AllurePytestRunner, severity): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + f""" + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.severity(allure.severity_level.{severity.name}) + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_severity(severity.value), + ) + ) diff --git a/tests/allure_pytest_bdd/acceptance/labels/stories_test.py b/tests/allure_pytest_bdd/acceptance/labels/stories_test.py new file mode 100644 index 00000000..3e78d8ec --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/labels/stories_test.py @@ -0,0 +1,81 @@ +from hamcrest import assert_that + +from allure_commons_test.report import has_test_case +from allure_commons_test.label import has_story + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_story_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.story("Lorem Ipsum") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_story("Lorem Ipsum"), + ) + ) + + +def test_dynamic_story(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.story("Lorem Ipsum") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_story("Lorem Ipsum"), + ) + ) diff --git a/tests/allure_pytest_bdd/acceptance/labels/sub_suites_test.py b/tests/allure_pytest_bdd/acceptance/labels/sub_suites_test.py new file mode 100644 index 00000000..a40c734c --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/labels/sub_suites_test.py @@ -0,0 +1,81 @@ +from hamcrest import assert_that + +from allure_commons_test.report import has_test_case +from allure_commons_test.label import has_sub_suite + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_sub_suite_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.sub_suite("Lorem Ipsum") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_sub_suite("Lorem Ipsum"), + ) + ) + + +def test_dynamic_sub_suite(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.sub_suite("Lorem Ipsum") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_sub_suite("Lorem Ipsum"), + ) + ) diff --git a/tests/allure_pytest_bdd/acceptance/labels/suites_test.py b/tests/allure_pytest_bdd/acceptance/labels/suites_test.py new file mode 100644 index 00000000..bcc69f6e --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/labels/suites_test.py @@ -0,0 +1,81 @@ +from hamcrest import assert_that + +from allure_commons_test.report import has_test_case +from allure_commons_test.label import has_suite + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_suite_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.suite("Lorem Ipsum") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_suite("Lorem Ipsum"), + ) + ) + + +def test_dynamic_suite(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.suite("Lorem Ipsum") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_suite("Lorem Ipsum"), + ) + ) From aa562bc686cd1adafefcc102409e058a9c930d6c Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Sat, 15 Mar 2025 00:09:06 +0700 Subject: [PATCH 10/26] feat(pytest-bdd): implement links --- allure-pytest-bdd/src/allure_api.py | 12 + allure-pytest-bdd/src/plugin.py | 2 + allure-pytest-bdd/src/pytest_bdd_listener.py | 2 + allure-pytest-bdd/src/utils.py | 17 ++ allure-python-commons-test/src/result.py | 13 +- .../acceptance/links/__init__.py | 0 .../acceptance/links/default_links_test.py | 231 ++++++++++++++++++ .../acceptance/links/issue_links_test.py | 156 ++++++++++++ .../acceptance/links/tms_links_test.py | 156 ++++++++++++ 9 files changed, 586 insertions(+), 3 deletions(-) create mode 100644 tests/allure_pytest_bdd/acceptance/links/__init__.py create mode 100644 tests/allure_pytest_bdd/acceptance/links/default_links_test.py create mode 100644 tests/allure_pytest_bdd/acceptance/links/issue_links_test.py create mode 100644 tests/allure_pytest_bdd/acceptance/links/tms_links_test.py diff --git a/allure-pytest-bdd/src/allure_api.py b/allure-pytest-bdd/src/allure_api.py index 6e8d2828..b70b19ba 100644 --- a/allure-pytest-bdd/src/allure_api.py +++ b/allure-pytest-bdd/src/allure_api.py @@ -2,11 +2,13 @@ import allure_commons from allure_commons.model2 import Label +from allure_commons.model2 import Link from .utils import ALLURE_TITLE_MARK from .utils import ALLURE_DESCRIPTION_MARK from .utils import ALLURE_DESCRIPTION_HTML_MARK from .utils import ALLURE_LABEL_MARK +from .utils import ALLURE_LINK_MARK class AllurePytestBddApi: @@ -52,3 +54,13 @@ def decorate_as_label(self, label_type, labels): def add_label(self, label_type, labels): with self.lifecycle.update_test_case() as test_result: test_result.labels.extend(Label(name=label_type, value=value) for value in labels or []) + + @allure_commons.hookimpl + def decorate_as_link(self, url, link_type, name): + allure_link_mark = getattr(pytest.mark, ALLURE_LINK_MARK) + return allure_link_mark(url, name=name, link_type=link_type) + + @allure_commons.hookimpl + def add_link(self, url, link_type, name): + with self.lifecycle.update_test_case() as test_result: + test_result.links.append(Link(url=url, name=name, type=link_type)) diff --git a/allure-pytest-bdd/src/plugin.py b/allure-pytest-bdd/src/plugin.py index 5a5c0615..04123512 100644 --- a/allure-pytest-bdd/src/plugin.py +++ b/allure-pytest-bdd/src/plugin.py @@ -9,6 +9,7 @@ from .utils import ALLURE_DESCRIPTION_MARK from .utils import ALLURE_DESCRIPTION_HTML_MARK from .utils import ALLURE_LABEL_MARK +from .utils import ALLURE_LINK_MARK def pytest_addoption(parser): @@ -37,6 +38,7 @@ def register_marks(config): config.addinivalue_line("markers", f"{ALLURE_DESCRIPTION_MARK}: allure description") config.addinivalue_line("markers", f"{ALLURE_DESCRIPTION_HTML_MARK}: allure description in HTML") config.addinivalue_line("markers", f"{ALLURE_LABEL_MARK}: allure label marker") + config.addinivalue_line("markers", f"{ALLURE_LINK_MARK}: allure link marker") def pytest_configure(config): diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index 7a75d216..f450e313 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -22,6 +22,7 @@ from .utils import get_pytest_params from .utils import convert_params from .utils import get_allure_labels +from .utils import get_allure_links from .utils import get_allure_description from .utils import get_allure_description_html @@ -60,6 +61,7 @@ def pytest_bdd_before_scenario(self, request, feature, scenario): test_result.labels.append(Label(name=LabelType.FRAMEWORK, value="pytest-bdd")) test_result.labels.append(Label(name=LabelType.LANGUAGE, value=platform_label())) test_result.labels.extend(get_allure_labels(item)) + test_result.links.extend(get_allure_links(item)) test_result.parameters = convert_params(params) finalizer = partial(self._scenario_finalizer, scenario) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index 2fb99aa8..a10fe7fe 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -4,6 +4,7 @@ from allure_commons.utils import md5 from allure_commons.utils import SafeFormatter from allure_commons.model2 import Label +from allure_commons.model2 import Link from allure_commons.model2 import StatusDetails from allure_commons.model2 import Status from allure_commons.model2 import Parameter @@ -16,6 +17,7 @@ ALLURE_DESCRIPTION_HTML_MARK = "allure_description_html" ALLURE_TITLE_MARK = "allure_title" ALLURE_LABEL_MARK = 'allure_label' +ALLURE_LINK_MARK = 'allure_link' MARK_NAMES_TO_IGNORE = { "usefixtures", @@ -92,6 +94,21 @@ def get_allure_labels(item): return convert_labels(iter_all_labels(item)) +def iter_all_links(item): + for marker in item.iter_markers(name=ALLURE_LINK_MARK): + url = marker.args[0] if marker and marker.args else None + if url: + yield url, marker.kwargs.get("name"), marker.kwargs.get("link_type") + + +def convert_links(links): + return [Link(url=url, name=name, type=link_type) for url, name, link_type in links] + + +def get_allure_links(item): + return convert_links(iter_all_links(item)) + + def resolve_description(description): if isinstance(description, str): return description diff --git a/allure-python-commons-test/src/result.py b/allure-python-commons-test/src/result.py index c9c3d18e..123e337a 100644 --- a/allure-python-commons-test/src/result.py +++ b/allure-python-commons-test/src/result.py @@ -62,8 +62,8 @@ """ -from hamcrest import all_of, anything, not_ -from hamcrest import equal_to, not_none +from hamcrest import all_of, anything, not_, any_of +from hamcrest import equal_to, none, not_none from hamcrest import has_entry, has_item from hamcrest import contains_string from allure_commons_test.lookup import maps_to @@ -122,13 +122,20 @@ def doesnt_have_parameter(name): )) +def resolve_link_attr_matcher(key, value): + return has_entry(key, value) if value is not None else any_of( + not_(has_entry(key)), + none(), + ) + + def has_link(url, link_type=None, name=None): return has_entry( 'links', has_item( all_of( *[ - has_entry(key, value) for key, value in zip( + resolve_link_attr_matcher(key, value) for key, value in zip( ('url', 'type', 'name'), (url, link_type, name) ) if value is not None diff --git a/tests/allure_pytest_bdd/acceptance/links/__init__.py b/tests/allure_pytest_bdd/acceptance/links/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/allure_pytest_bdd/acceptance/links/default_links_test.py b/tests/allure_pytest_bdd/acceptance/links/default_links_test.py new file mode 100644 index 00000000..8e307584 --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/links/default_links_test.py @@ -0,0 +1,231 @@ +from hamcrest import assert_that + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_link + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_link_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.link("https://allurereport.org") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_link("https://allurereport.org", link_type="link"), + ), + ) + + +def test_named_link_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.link("https://allurereport.org", name="foo") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_link("https://allurereport.org", link_type="link", name="foo"), + ), + ) + + +def test_custom_type_link_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.link("https://allurereport.org", link_type="foo") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_link("https://allurereport.org", link_type="foo"), + ), + ) + + +def test_dynamic_link(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.link("https://allurereport.org") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_link("https://allurereport.org", link_type="link"), + ), + ) + + +def test_named_dynamic_link(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.link("https://allurereport.org", name="foo") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_link("https://allurereport.org", link_type="link", name="foo"), + ), + ) + + +def test_custom_type_dynamic_link(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.link("https://allurereport.org", link_type="foo") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_link("https://allurereport.org", link_type="foo"), + ), + ) diff --git a/tests/allure_pytest_bdd/acceptance/links/issue_links_test.py b/tests/allure_pytest_bdd/acceptance/links/issue_links_test.py new file mode 100644 index 00000000..2c32440a --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/links/issue_links_test.py @@ -0,0 +1,156 @@ +from hamcrest import assert_that + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_issue_link + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_issue_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.issue("https://allurereport.org") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_issue_link("https://allurereport.org"), + ), + ) + + +def test_named_issue_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.issue("https://allurereport.org", name="foo") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_issue_link("https://allurereport.org", name="foo"), + ), + ) + + +def test_dynamic_issue(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.issue("https://allurereport.org") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_issue_link("https://allurereport.org"), + ), + ) + + +def test_named_dynamic_issue(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.issue("https://allurereport.org", name="foo") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_issue_link("https://allurereport.org", name="foo"), + ), + ) diff --git a/tests/allure_pytest_bdd/acceptance/links/tms_links_test.py b/tests/allure_pytest_bdd/acceptance/links/tms_links_test.py new file mode 100644 index 00000000..137d6208 --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/links/tms_links_test.py @@ -0,0 +1,156 @@ +from hamcrest import assert_that + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_test_case_link + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_tms_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.testcase("https://allurereport.org") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_test_case_link("https://allurereport.org"), + ), + ) + + +def test_named_tms_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.testcase("https://allurereport.org", name="foo") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_test_case_link("https://allurereport.org", name="foo"), + ), + ) + + +def test_dynamic_tms(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.testcase("https://allurereport.org") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_test_case_link("https://allurereport.org"), + ), + ) + + +def test_named_dynamic_tms(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.testcase("https://allurereport.org", name="foo") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_test_case_link("https://allurereport.org", name="foo"), + ), + ) From 507be822584a5f5136dafbffe7d9a32a4d5171cc Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Sat, 15 Mar 2025 01:33:09 +0700 Subject: [PATCH 11/26] feat(pytest-bdd): implement --allure-link-pattern --- allure-pytest-bdd/src/allure_api.py | 8 +- allure-pytest-bdd/src/plugin.py | 27 ++- allure-pytest-bdd/src/utils.py | 28 +++ .../acceptance/links/link_templates_test.py | 216 ++++++++++++++++++ 4 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 tests/allure_pytest_bdd/acceptance/links/link_templates_test.py diff --git a/allure-pytest-bdd/src/allure_api.py b/allure-pytest-bdd/src/allure_api.py index b70b19ba..24e1c672 100644 --- a/allure-pytest-bdd/src/allure_api.py +++ b/allure-pytest-bdd/src/allure_api.py @@ -10,10 +10,14 @@ from .utils import ALLURE_LABEL_MARK from .utils import ALLURE_LINK_MARK +from .utils import get_link_patterns +from .utils import apply_link_pattern + class AllurePytestBddApi: - def __init__(self, lifecycle): + def __init__(self, config, lifecycle): self.lifecycle = lifecycle + self.__link_patterns = get_link_patterns(config) @allure_commons.hookimpl def decorate_as_title(self, test_title): @@ -57,10 +61,12 @@ def add_label(self, label_type, labels): @allure_commons.hookimpl def decorate_as_link(self, url, link_type, name): + url = apply_link_pattern(self.__link_patterns, link_type, url) allure_link_mark = getattr(pytest.mark, ALLURE_LINK_MARK) return allure_link_mark(url, name=name, link_type=link_type) @allure_commons.hookimpl def add_link(self, url, link_type, name): + url = apply_link_pattern(self.__link_patterns, link_type, url) with self.lifecycle.update_test_case() as test_result: test_result.links.append(Link(url=url, name=name, type=link_type)) diff --git a/allure-pytest-bdd/src/plugin.py b/allure-pytest-bdd/src/plugin.py index 04123512..a400139f 100644 --- a/allure-pytest-bdd/src/plugin.py +++ b/allure-pytest-bdd/src/plugin.py @@ -1,5 +1,7 @@ -import allure_commons +import argparse import os + +import allure_commons from allure_commons.logger import AllureFileLogger from allure_commons.lifecycle import AllureLifecycle @@ -25,6 +27,27 @@ def pytest_addoption(parser): dest="clean_alluredir", help="Clean alluredir folder if it exists") + def link_pattern(string): + pattern = string.split(':', 1) + if not pattern[0]: + raise argparse.ArgumentTypeError("A link type is mandatory") + + if len(pattern) != 2: + raise argparse.ArgumentTypeError("A link pattern is mandatory") + return pattern + + parser.getgroup("general").addoption( + "--allure-link-pattern", + action="append", + dest="allure_link_pattern", + metavar="LINK_TYPE:LINK_PATTERN", + default=[], + type=link_pattern, + help="""A URL pattern for a link type. Allows short links in tests, + e.g., 'issue-1'. `pattern.format(short_url)` will be called to get + the full URL""" + ) + def cleanup_factory(plugin): def clean_up(): @@ -57,7 +80,7 @@ def pytest_configure(config): allure_commons.plugin_manager.register(pytest_bdd_listener) config.add_cleanup(cleanup_factory(pytest_bdd_listener)) - allure_api_impl = AllurePytestBddApi(lifecycle) + allure_api_impl = AllurePytestBddApi(config, lifecycle) allure_commons.plugin_manager.register(allure_api_impl) config.add_cleanup(cleanup_factory(allure_api_impl)) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index a10fe7fe..f0a9ca6b 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -1,5 +1,6 @@ import os import pytest +from urllib.parse import urlparse from uuid import UUID from allure_commons.utils import md5 from allure_commons.utils import SafeFormatter @@ -9,6 +10,7 @@ from allure_commons.model2 import Status from allure_commons.model2 import Parameter from allure_commons.types import LabelType +from allure_commons.types import LinkType from allure_commons.utils import format_exception ALLURE_PYTEST_BDD_HASHKEY = pytest.StashKey() @@ -94,6 +96,32 @@ def get_allure_labels(item): return convert_labels(iter_all_labels(item)) +def get_link_patterns(config): + patterns = {} + for link_type, pattern in config.option.allure_link_pattern: + patterns[link_type] = pattern + return patterns + + +def is_url(maybeUrl): + try: + result = urlparse(maybeUrl) + except AttributeError: + return False + + return result and ( + getattr(result, "scheme", None) or getattr(result, "netloc", None) + ) + + +def apply_link_pattern(patterns, link_type, url): + if is_url(url): + return url + + pattern = patterns.get(link_type or LinkType.LINK) + return url if pattern is None else pattern.format(url) + + def iter_all_links(item): for marker in item.iter_markers(name=ALLURE_LINK_MARK): url = marker.args[0] if marker and marker.args else None diff --git a/tests/allure_pytest_bdd/acceptance/links/link_templates_test.py b/tests/allure_pytest_bdd/acceptance/links/link_templates_test.py new file mode 100644 index 00000000..d00d5d67 --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/links/link_templates_test.py @@ -0,0 +1,216 @@ +import pytest +from hamcrest import assert_that + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_link +from allure_commons_test.result import has_issue_link + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_decorator_link_formatted(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.issue("726") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + cli_args=["--allure-link-pattern", "issue:https://github.com/allure-framework/allure-python/issues/{}"], + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_issue_link("https://github.com/allure-framework/allure-python/issues/726"), + ), + ) + + +def test_dynamic_link_formatted(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.issue("726") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + cli_args=["--allure-link-pattern", "issue:https://github.com/allure-framework/allure-python/issues/{}"], + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_issue_link("https://github.com/allure-framework/allure-python/issues/726"), + ), + ) + + +def test_type_mismatch_unchanged(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.link("726", link_type="foo") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + cli_args=["--allure-link-pattern", "link:https://github.com/allure-framework/allure-python/issues/{}"], + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_link("726", link_type="foo"), + ), + ) + + +def test_multiple_patterns_allowed(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.issue("726", name="issue-726") + @allure.link("pytestbdd", link_type="framework", name="docs") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + cli_args=[ + "--allure-link-pattern", + "framework:https://allurereport.org/docs/{}/", + "--allure-link-pattern", + "issue:https://github.com/allure-framework/allure-python/issues/{}", + ], + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_issue_link("https://github.com/allure-framework/allure-python/issues/726", name="issue-726"), + has_link("https://allurereport.org/docs/pytestbdd/", name="docs", link_type="framework"), + ), + ) + + +@pytest.mark.parametrize("url", [ + "http://foo", + "https://foo", + "ftp://foo", + "file:///foo", + "customapp:custompath?foo=bar&baz=qux", +]) +def test_full_urls_not_formatted(allure_pytest_bdd_runner: AllurePytestRunner, url): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + f""" + from pytest_bdd import scenario, given + import allure + + @allure.link("{url}") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + cli_args=["--allure-link-pattern", "link:https://allurereport.org/{}/"], + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_link(url), + ), + ) From f29fa77fc42878db857ef83e5a3e09cb1fc62565 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Sat, 15 Mar 2025 03:04:05 +0700 Subject: [PATCH 12/26] feat(pytest-bdd): implement dynamic parameters --- allure-pytest-bdd/src/allure_api.py | 14 + allure-pytest-bdd/src/pytest_bdd_listener.py | 22 +- allure-pytest-bdd/src/types.py | 7 + allure-pytest-bdd/src/utils.py | 66 ++- allure-python-commons-test/src/result.py | 4 +- .../acceptance/parameters_test.py | 415 ++++++++++++++++++ 6 files changed, 506 insertions(+), 22 deletions(-) create mode 100644 allure-pytest-bdd/src/types.py create mode 100644 tests/allure_pytest_bdd/acceptance/parameters_test.py diff --git a/allure-pytest-bdd/src/allure_api.py b/allure-pytest-bdd/src/allure_api.py index 24e1c672..78f39bd0 100644 --- a/allure-pytest-bdd/src/allure_api.py +++ b/allure-pytest-bdd/src/allure_api.py @@ -3,6 +3,8 @@ import allure_commons from allure_commons.model2 import Label from allure_commons.model2 import Link +from allure_commons.model2 import Parameter +from allure_commons.utils import represent from .utils import ALLURE_TITLE_MARK from .utils import ALLURE_DESCRIPTION_MARK @@ -70,3 +72,15 @@ def add_link(self, url, link_type, name): url = apply_link_pattern(self.__link_patterns, link_type, url) with self.lifecycle.update_test_case() as test_result: test_result.links.append(Link(url=url, name=name, type=link_type)) + + @allure_commons.hookimpl + def add_parameter(self, name, value, excluded, mode): + with self.lifecycle.update_test_case() as test_result: + test_result.parameters.append( + Parameter( + name=name, + value=represent(value), + excluded=excluded, + mode=mode.value if mode else None, + ), + ) diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index f450e313..db1ea501 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -11,7 +11,7 @@ from allure_commons.utils import host_tag, thread_tag from allure_commons.utils import md5 -from .utils import set_feature_and_scenario +from .utils import save_test_data from .utils import post_process_test_result from .utils import get_uuid from .utils import get_step_name @@ -19,6 +19,7 @@ from .utils import get_pytest_report_status from .utils import get_full_name from .utils import get_test_name +from .utils import get_outline_params from .utils import get_pytest_params from .utils import convert_params from .utils import get_allure_labels @@ -46,23 +47,32 @@ def _scenario_finalizer(self, scenario): @pytest.hookimpl def pytest_bdd_before_scenario(self, request, feature, scenario): item = request.node - set_feature_and_scenario(item, feature, scenario) uuid = get_uuid(item.nodeid) - params = get_pytest_params(item) + outline_params = get_outline_params(item) + pytest_params = get_pytest_params(item) + params = { **pytest_params, **outline_params } + save_test_data( + item=item, + feature=feature, + scenario=scenario, + pytest_params=pytest_params, + ) + + full_name = get_full_name(feature, scenario) with self.lifecycle.schedule_test_case(uuid=uuid) as test_result: - test_result.fullName = get_full_name(feature, scenario) + test_result.fullName = full_name 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) test_result.start = now() - test_result.historyId = md5(item.nodeid) + test_result.testCaseId = md5(full_name) test_result.labels.append(Label(name=LabelType.HOST, value=self.host)) test_result.labels.append(Label(name=LabelType.THREAD, value=self.thread)) test_result.labels.append(Label(name=LabelType.FRAMEWORK, value="pytest-bdd")) test_result.labels.append(Label(name=LabelType.LANGUAGE, value=platform_label())) test_result.labels.extend(get_allure_labels(item)) test_result.links.extend(get_allure_links(item)) - test_result.parameters = convert_params(params) + test_result.parameters.extend(convert_params(outline_params, pytest_params)) finalizer = partial(self._scenario_finalizer, scenario) item.addfinalizer(finalizer) diff --git a/allure-pytest-bdd/src/types.py b/allure-pytest-bdd/src/types.py new file mode 100644 index 00000000..4dccb1b6 --- /dev/null +++ b/allure-pytest-bdd/src/types.py @@ -0,0 +1,7 @@ +from collections import namedtuple + + +AllurePytestBddTestData = namedtuple( + "AllurePytestBddTestData", + ["feature", "scenario", "pytest_params"], +) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index f0a9ca6b..2f46bd5a 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -2,8 +2,6 @@ import pytest from urllib.parse import urlparse from uuid import UUID -from allure_commons.utils import md5 -from allure_commons.utils import SafeFormatter from allure_commons.model2 import Label from allure_commons.model2 import Link from allure_commons.model2 import StatusDetails @@ -12,6 +10,11 @@ from allure_commons.types import LabelType from allure_commons.types import LinkType from allure_commons.utils import format_exception +from allure_commons.utils import md5 +from allure_commons.utils import represent +from allure_commons.utils import SafeFormatter + +from .types import AllurePytestBddTestData ALLURE_PYTEST_BDD_HASHKEY = pytest.StashKey() @@ -31,11 +34,15 @@ } -def set_feature_and_scenario(item, feature, scenario): - item.stash[ALLURE_PYTEST_BDD_HASHKEY] = (feature, scenario) +def save_test_data(item, feature, scenario, pytest_params): + item.stash[ALLURE_PYTEST_BDD_HASHKEY] = AllurePytestBddTestData( + feature=feature, + scenario=scenario, + pytest_params=pytest_params, + ) -def get_feature_and_scenario(item): +def get_test_data(item): return item.stash.get(ALLURE_PYTEST_BDD_HASHKEY, (None, None)) @@ -190,37 +197,68 @@ def get_pytest_report_status(pytest_report): return status +def get_outline_params(node): + if hasattr(node, 'callspec'): + return node.callspec.params.get('_pytest_bdd_example', {}) + return {} + + def get_pytest_params(node): if hasattr(node, 'callspec'): pytest_params = dict(node.callspec.params) - pytest_bdd_params = pytest_params.pop('_pytest_bdd_example', {}) - return {**pytest_bdd_params, **pytest_params} + if "_pytest_bdd_example" in pytest_params: + del pytest_params["_pytest_bdd_example"] + return pytest_params + return {} -def convert_params(pytest_params): +def convert_params(outline_params, pytest_params): return [ - Parameter( + *(Parameter( name=name, value=value, - ) for name, value in (pytest_params or {}).items() + ) for name, value in outline_params.items()), + *(Parameter( + name=name, + value=represent(value), + ) for name, value in pytest_params.items() if name not in outline_params), ] def iter_pytest_labels(item, test_result): - feature, _ = get_feature_and_scenario(item) + test_data = get_test_data(item) existing_labels = {label.name for label in test_result.labels} if LabelType.FEATURE not in existing_labels: - yield LabelType.FEATURE, feature.name + yield LabelType.FEATURE, test_data.feature.name yield from iter_pytest_tags(item) -def post_process_test_result(item, test_result): - test_result.labels.extend( +def iter_default_labels(item, test_result): + return ( Label( name=name, value=value, ) for name, value in iter_pytest_labels(item, test_result) ) + + +def get_history_id(test_case_id, parameters, pytest_params): + parameters_part = md5(*(pytest_params.get(p.name, p.value) for p in sorted( + filter(lambda p: not p.excluded, parameters), + key=lambda p: p.name, + ))) + return f"{test_case_id}.{parameters_part}" + + +def post_process_test_result(item, test_result): + test_data = get_test_data(item) + + test_result.labels.extend(iter_default_labels(item, test_result)) + test_result.historyId = get_history_id( + test_case_id=test_result.testCaseId, + parameters=test_result.parameters, + pytest_params=test_data.pytest_params, + ) diff --git a/allure-python-commons-test/src/result.py b/allure-python-commons-test/src/result.py index 123e337a..7a740cb3 100644 --- a/allure-python-commons-test/src/result.py +++ b/allure-python-commons-test/src/result.py @@ -212,5 +212,5 @@ def with_mode(mode): return has_entry('mode', mode) -def has_history_id(): - return has_entry('historyId', anything()) +def has_history_id(matcher=None): + return has_entry('historyId', matcher or anything()) diff --git a/tests/allure_pytest_bdd/acceptance/parameters_test.py b/tests/allure_pytest_bdd/acceptance/parameters_test.py new file mode 100644 index 00000000..24f61931 --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/parameters_test.py @@ -0,0 +1,415 @@ +from hamcrest import assert_that +from hamcrest import all_of +from hamcrest import equal_to +from hamcrest import not_ +from hamcrest import has_length + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_parameter +from allure_commons_test.result import with_mode +from allure_commons_test.result import with_excluded +from allure_commons_test.result import has_history_id + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_parameter_added(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.parameter("foo", "bar") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_parameter("foo", "'bar'"), + ), + ) + + +def test_masked_parameter(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.parameter("foo", "bar", mode=allure.parameter_mode.MASKED) + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_parameter("foo", "'bar'", with_mode("masked")), + ), + ) + + +def test_hidden_parameter(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.parameter("foo", "bar", mode=allure.parameter_mode.HIDDEN) + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_parameter("foo", "'bar'", with_mode("hidden")), + ), + ) + + +def test_excluded_parameter(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.parameter("foo", "bar", excluded=True) + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_parameter("foo", "'bar'", with_excluded()), + ), + ) + + +def test_parameters_affect_history_id(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + impl_with_no_parameter = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + impl_with_parameter = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.parameter("foo", "bar") + + @given("noop") + def given_noop(): + pass + """ + ) + + results_with_no_parameter = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + impl_with_no_parameter, + ) + + results_with_parameter = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + impl_with_parameter, + ) + + assert_that( + results_with_parameter, + has_test_case( + "sample.feature:Bar", + has_history_id( + not_(equal_to(results_with_no_parameter.test_cases[0]["historyId"])), + ), + ), + ) + + +def test_parameters_order_doesnt_matter(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + impl_order1 = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.parameter("baz", "qux") + allure.dynamic.parameter("foo", "bar") + + @given("noop") + def given_noop(): + pass + """ + ) + impl_order2 = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.parameter("foo", "bar") + allure.dynamic.parameter("baz", "qux") + + @given("noop") + def given_noop(): + pass + """ + ) + + results_order1 = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + impl_order1, + ) + + results_order2 = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + impl_order2, + ) + + assert_that( + results_order1, + has_test_case( + "sample.feature:Bar", + has_history_id( + equal_to(results_order2.test_cases[0]["historyId"]), + ), + ), + ) + + +def test_excluded_parameters_doesnt_affect_history_id(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + impl_no_parameter = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + impl_excluded_parameter = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + allure.dynamic.parameter("foo", "bar", excluded=True) + + @given("noop") + def given_noop(): + pass + """ + ) + + results_no_parameter = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + impl_no_parameter, + ) + + results_excluded_parameter = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + impl_excluded_parameter, + ) + + assert_that( + results_no_parameter, + has_test_case( + "sample.feature:Bar", + has_history_id( + equal_to(results_excluded_parameter.test_cases[0]["historyId"]), + ), + ), + ) + + +def test_pytest_parameters_added(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + impl_content = ( + """ + import pytest + from pytest_bdd import scenario, given + + @pytest.mark.parametrize("foo", ["bar", {"baz": "qux"}]) + @scenario("sample.feature", "Bar") + def test_scenario(foo): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + impl_content, + ) + + assert_that( + allure_results, + all_of( + has_test_case( + "sample.feature:Bar", + has_parameter("foo", "'bar'"), + ), + has_test_case( + "sample.feature:Bar", + has_parameter("foo", "{'baz': 'qux'}"), + ), + ), + ) + + +def test_original_pytest_parameter_values_used_to_get_history_id(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + impl_content = ( + """ + import pytest + from pytest_bdd import scenario, given + + @pytest.mark.parametrize("foo", [b"bar", b"baz"]) + @scenario("sample.feature", "Bar") + def test_scenario(foo): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + impl_content, + ) + + history_ids = {tc["historyId"] for tc in allure_results.test_cases} + + assert_that(history_ids, has_length(2)) From d147a4864209ffd668e00ef99d84eeed6e912d11 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Sat, 15 Mar 2025 03:36:14 +0700 Subject: [PATCH 13/26] feat(pytest-bdd): implement substeps (WIP) --- allure-pytest-bdd/src/allure_api.py | 20 ++ allure-pytest-bdd/src/utils.py | 18 ++ .../acceptance/steps_test.py | 198 ++++++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 tests/allure_pytest_bdd/acceptance/steps_test.py diff --git a/allure-pytest-bdd/src/allure_api.py b/allure-pytest-bdd/src/allure_api.py index 78f39bd0..df8f8e6a 100644 --- a/allure-pytest-bdd/src/allure_api.py +++ b/allure-pytest-bdd/src/allure_api.py @@ -14,6 +14,8 @@ from .utils import get_link_patterns from .utils import apply_link_pattern +from .utils import get_status +from .utils import get_status_details_for_step class AllurePytestBddApi: @@ -84,3 +86,21 @@ def add_parameter(self, name, value, excluded, mode): mode=mode.value if mode else None, ), ) + + @allure_commons.hookimpl + def start_step(self, uuid, title, params): + with self.lifecycle.start_step(uuid=uuid) as step_result: + step_result.name = title + step_result.parameters.extend( + Parameter( + name=name, + value=represent(value), + ) for name, value in params.items() + ) + + @allure_commons.hookimpl + def stop_step(self, uuid, exc_type, exc_val, exc_tb): + with self.lifecycle.update_step(uuid=uuid) as step_result: + step_result.status = get_status(exc_val) + step_result.statusDetails = get_status_details_for_step(exc_type, exc_val, exc_tb) + self.lifecycle.stop_step(uuid=uuid) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index 2f46bd5a..5ec63ddd 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -10,6 +10,7 @@ from allure_commons.types import LabelType from allure_commons.types import LinkType from allure_commons.utils import format_exception +from allure_commons.utils import format_traceback from allure_commons.utils import md5 from allure_commons.utils import represent from allure_commons.utils import SafeFormatter @@ -183,12 +184,29 @@ def get_uuid(*args): return str(UUID(md5(*args))) +def get_status(exception): + if exception: + if isinstance(exception, (AssertionError, pytest.fail.Exception)): + return Status.FAILED + elif isinstance(exception, pytest.skip.Exception): + return Status.SKIPPED + return Status.BROKEN + else: + return Status.PASSED + + def get_status_details(exception): message = str(exception) trace = format_exception(type(exception), exception) return StatusDetails(message=message, trace=trace) if message or trace else None +def get_status_details_for_step(exception_type, exception, exception_traceback): + message = format_exception(exception_type, exception) + trace = format_traceback(exception_traceback) + return StatusDetails(message=message, trace=trace) if message or trace else None + + def get_pytest_report_status(pytest_report): pytest_statuses = ('failed', 'passed', 'skipped') statuses = (Status.FAILED, Status.PASSED, Status.SKIPPED) diff --git a/tests/allure_pytest_bdd/acceptance/steps_test.py b/tests/allure_pytest_bdd/acceptance/steps_test.py new file mode 100644 index 00000000..f80c2108 --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/steps_test.py @@ -0,0 +1,198 @@ +from hamcrest import assert_that + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_step +from allure_commons_test.result import with_status +from allure_commons_test.result import has_parameter +from allure_commons_test.result import has_status_details +from allure_commons_test.result import with_message_contains +from allure_commons_test.result import with_trace_contains + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_one_context_substep(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + with allure.step("foo"): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given noop", + has_step( + "foo", + with_status("passed"), + ), + ), + ), + ) + + +def test_one_function_substep(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.step("foo") + def fn(): + pass + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + fn() + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given noop", + has_step( + "foo", + with_status("passed"), + ), + ), + ), + ) + + +def test_substep_with_parameters(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + step = allure.step("foo") + step.params = {"foo": "bar", "baz": {"qux": "qut"}} + with step: + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given noop", + has_step( + "foo", + with_status("passed"), + has_parameter("foo", "'bar'"), + has_parameter("baz", "{'qux': 'qut'}"), + ), + ), + ), + ) + + +def test_failed_substep(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + with allure.step("foo"): + assert False + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given noop", + has_step( + "foo", + with_status("failed"), + has_status_details( + with_message_contains("AssertionError: assert False"), + with_trace_contains("in given_noop"), + ), + ), + ), + ), + ) From 1f996bf411ebda1394093d0a7ae6c8347a14be1d Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 19 Mar 2025 03:58:28 +0700 Subject: [PATCH 14/26] feat(pytest-bdd): improve steps reporting --- .../src/{allure_api.py => api.py} | 78 ++- allure-pytest-bdd/src/plugin.py | 12 +- allure-pytest-bdd/src/pytest_bdd_listener.py | 68 ++- allure-pytest-bdd/src/steps.py | 113 +++++ allure-pytest-bdd/src/storage.py | 29 ++ allure-pytest-bdd/src/types.py | 12 +- allure-pytest-bdd/src/utils.py | 89 +--- allure-python-commons-test/src/result.py | 8 + .../acceptance/steps/__init__.py | 0 .../acceptance/steps/api_steps_test.py | 380 ++++++++++++++ .../acceptance/steps/gherkin_steps_test.py | 475 ++++++++++++++++++ .../acceptance/steps_test.py | 198 -------- 12 files changed, 1118 insertions(+), 344 deletions(-) rename allure-pytest-bdd/src/{allure_api.py => api.py} (65%) create mode 100644 allure-pytest-bdd/src/steps.py create mode 100644 allure-pytest-bdd/src/storage.py create mode 100644 tests/allure_pytest_bdd/acceptance/steps/__init__.py create mode 100644 tests/allure_pytest_bdd/acceptance/steps/api_steps_test.py create mode 100644 tests/allure_pytest_bdd/acceptance/steps/gherkin_steps_test.py delete mode 100644 tests/allure_pytest_bdd/acceptance/steps_test.py diff --git a/allure-pytest-bdd/src/allure_api.py b/allure-pytest-bdd/src/api.py similarity index 65% rename from allure-pytest-bdd/src/allure_api.py rename to allure-pytest-bdd/src/api.py index df8f8e6a..2215ceb0 100644 --- a/allure-pytest-bdd/src/allure_api.py +++ b/allure-pytest-bdd/src/api.py @@ -1,21 +1,25 @@ import pytest import allure_commons + from allure_commons.model2 import Label from allure_commons.model2 import Link from allure_commons.model2 import Parameter from allure_commons.utils import represent -from .utils import ALLURE_TITLE_MARK -from .utils import ALLURE_DESCRIPTION_MARK -from .utils import ALLURE_DESCRIPTION_HTML_MARK -from .utils import ALLURE_LABEL_MARK -from .utils import ALLURE_LINK_MARK - -from .utils import get_link_patterns from .utils import apply_link_pattern -from .utils import get_status -from .utils import get_status_details_for_step +from .utils import get_link_patterns +from .utils import get_marker_value +from .utils import resolve_description +from .steps import start_step +from .steps import stop_step + + +ALLURE_DESCRIPTION_MARK = "allure_description" +ALLURE_DESCRIPTION_HTML_MARK = "allure_description_html" +ALLURE_TITLE_MARK = "allure_title" +ALLURE_LABEL_MARK = 'allure_label' +ALLURE_LINK_MARK = 'allure_link' class AllurePytestBddApi: @@ -89,18 +93,50 @@ def add_parameter(self, name, value, excluded, mode): @allure_commons.hookimpl def start_step(self, uuid, title, params): - with self.lifecycle.start_step(uuid=uuid) as step_result: - step_result.name = title - step_result.parameters.extend( - Parameter( - name=name, - value=represent(value), - ) for name, value in params.items() - ) + start_step(self.lifecycle, step_uuid=uuid, title=title, params=params) @allure_commons.hookimpl def stop_step(self, uuid, exc_type, exc_val, exc_tb): - with self.lifecycle.update_step(uuid=uuid) as step_result: - step_result.status = get_status(exc_val) - step_result.statusDetails = get_status_details_for_step(exc_type, exc_val, exc_tb) - self.lifecycle.stop_step(uuid=uuid) + stop_step( + self.lifecycle, + uuid, + exception=exc_val, + exception_type=exc_type, + traceback=exc_tb, + ) + + +def get_allure_title(item): + return get_marker_value(item, ALLURE_TITLE_MARK) + + +def get_allure_description(item, feature, scenario): + value = get_marker_value(item, ALLURE_DESCRIPTION_MARK) + if value: + return value + + feature_description = resolve_description(feature.description) + scenario_description = resolve_description(scenario.description) + return "\n\n".join(filter(None, [feature_description, scenario_description])) + + +def get_allure_description_html(item): + return get_marker_value(item, ALLURE_DESCRIPTION_HTML_MARK) + + +def iter_all_labels(item): + for mark in item.iter_markers(name=ALLURE_LABEL_MARK): + name = mark.kwargs.get("label_type") + if name: + yield from ((name, value) for value in mark.args or []) + + +def iter_label_values(item, name): + return (pair for pair in iter_all_labels(item) if pair[0] == name) + + +def iter_all_links(item): + for marker in item.iter_markers(name=ALLURE_LINK_MARK): + url = marker.args[0] if marker and marker.args else None + if url: + yield url, marker.kwargs.get("name"), marker.kwargs.get("link_type") diff --git a/allure-pytest-bdd/src/plugin.py b/allure-pytest-bdd/src/plugin.py index a400139f..b8a6d798 100644 --- a/allure-pytest-bdd/src/plugin.py +++ b/allure-pytest-bdd/src/plugin.py @@ -5,13 +5,13 @@ from allure_commons.logger import AllureFileLogger from allure_commons.lifecycle import AllureLifecycle -from .allure_api import AllurePytestBddApi +from .api import AllurePytestBddApi from .pytest_bdd_listener import PytestBDDListener -from .utils import ALLURE_TITLE_MARK -from .utils import ALLURE_DESCRIPTION_MARK -from .utils import ALLURE_DESCRIPTION_HTML_MARK -from .utils import ALLURE_LABEL_MARK -from .utils import ALLURE_LINK_MARK +from .api import ALLURE_TITLE_MARK +from .api import ALLURE_DESCRIPTION_MARK +from .api import ALLURE_DESCRIPTION_HTML_MARK +from .api import ALLURE_LABEL_MARK +from .api import ALLURE_LINK_MARK def pytest_addoption(parser): diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index db1ea501..51d2f0b0 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -11,21 +11,25 @@ from allure_commons.utils import host_tag, thread_tag from allure_commons.utils import md5 -from .utils import save_test_data -from .utils import post_process_test_result -from .utils import get_uuid -from .utils import get_step_name -from .utils import get_status_details -from .utils import get_pytest_report_status -from .utils import get_full_name -from .utils import get_test_name -from .utils import get_outline_params -from .utils import get_pytest_params +from .api import get_allure_description +from .api import get_allure_description_html +from .steps import get_step_uuid +from .steps import report_remaining_steps +from .steps import report_undefined_step +from .steps import start_gherkin_step +from .steps import stop_gherkin_step +from .storage import save_excinfo +from .storage import save_test_data from .utils import convert_params from .utils import get_allure_labels from .utils import get_allure_links -from .utils import get_allure_description -from .utils import get_allure_description_html +from .utils import get_full_name +from .utils import get_outline_params +from .utils import get_pytest_params +from .utils import get_pytest_report_status +from .utils import get_test_name +from .utils import get_uuid +from .utils import post_process_test_result from functools import partial @@ -36,14 +40,6 @@ def __init__(self, lifecycle): self.host = host_tag() self.thread = thread_tag() - def _scenario_finalizer(self, scenario): - for step in scenario.steps: - step_uuid = get_uuid(str(id(step))) - with self.lifecycle.update_step(uuid=step_uuid) as step_result: - if step_result: - step_result.status = Status.SKIPPED - self.lifecycle.stop_step(uuid=step_uuid) - @pytest.hookimpl def pytest_bdd_before_scenario(self, request, feature, scenario): item = request.node @@ -74,7 +70,7 @@ def pytest_bdd_before_scenario(self, request, feature, scenario): test_result.links.extend(get_allure_links(item)) test_result.parameters.extend(convert_params(outline_params, pytest_params)) - finalizer = partial(self._scenario_finalizer, scenario) + finalizer = partial(report_remaining_steps, self.lifecycle, item) item.addfinalizer(finalizer) @pytest.hookimpl @@ -85,32 +81,19 @@ def pytest_bdd_after_scenario(self, request, feature, scenario): @pytest.hookimpl def pytest_bdd_before_step(self, request, feature, scenario, step, step_func): - parent_uuid = get_uuid(request.node.nodeid) - uuid = get_uuid(str(id(step))) - with self.lifecycle.start_step(parent_uuid=parent_uuid, uuid=uuid) as step_result: - step_result.name = get_step_name(step) + start_gherkin_step(self.lifecycle, request.node, step) @pytest.hookimpl def pytest_bdd_after_step(self, request, feature, scenario, step, step_func, step_func_args): - uuid = get_uuid(str(id(step))) - with self.lifecycle.update_step(uuid=uuid) as step_result: - step_result.status = Status.PASSED - self.lifecycle.stop_step(uuid=uuid) + stop_gherkin_step(self.lifecycle, request.node, get_step_uuid(step)) @pytest.hookimpl def pytest_bdd_step_error(self, request, feature, scenario, step, step_func, step_func_args, exception): - uuid = get_uuid(str(id(step))) - with self.lifecycle.update_step(uuid=uuid) as step_result: - step_result.status = Status.FAILED - step_result.statusDetails = get_status_details(exception) - self.lifecycle.stop_step(uuid=uuid) + stop_gherkin_step(self.lifecycle, request.node, get_step_uuid(step), exception=exception) @pytest.hookimpl def pytest_bdd_step_func_lookup_error(self, request, feature, scenario, step, exception): - uuid = get_uuid(str(id(step))) - with self.lifecycle.update_step(uuid=uuid) as step_result: - step_result.status = Status.BROKEN - self.lifecycle.stop_step(uuid=uuid) + report_undefined_step(self.lifecycle, request.node, step, exception) @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(self, item, call): @@ -118,9 +101,12 @@ def pytest_runtest_makereport(self, item, call): status = get_pytest_report_status(report) + excinfo = call.excinfo + status_details = StatusDetails( - message=call.excinfo.exconly(), - trace=report.longreprtext) if call.excinfo else None + message=excinfo.exconly(), + trace=report.longreprtext, + ) if excinfo else None uuid = get_uuid(report.nodeid) with self.lifecycle.update_test_case(uuid=uuid) as test_result: @@ -130,6 +116,8 @@ def pytest_runtest_makereport(self, item, call): test_result.statusDetails = status_details if report.when == "call" and test_result: + print("in makereport", excinfo) + save_excinfo(item, excinfo) if test_result.status not in [Status.PASSED, Status.FAILED]: test_result.status = status test_result.statusDetails = status_details diff --git a/allure-pytest-bdd/src/steps.py b/allure-pytest-bdd/src/steps.py new file mode 100644 index 00000000..542501d5 --- /dev/null +++ b/allure-pytest-bdd/src/steps.py @@ -0,0 +1,113 @@ +from allure_commons.model2 import StatusDetails +from allure_commons.model2 import Status +from allure_commons.model2 import Parameter +from allure_commons.utils import format_exception +from allure_commons.utils import represent + +from .storage import save_reported_step +from .utils import get_uuid +from .utils import get_status +from .utils import get_status_details +from .utils import get_test_data + + +def get_step_name(step): + return f"{step.keyword} {step.name}" + + +def get_step_uuid(step): + return get_uuid(str(id(step))) + + +def start_step(lifecycle, step_uuid, title, params=None, parent_uuid=None): + with lifecycle.start_step(uuid=step_uuid, parent_uuid=parent_uuid) as step_result: + step_result.name = title + if params: + step_result.parameters.extend( + Parameter( + name=name, + value=represent(value), + ) for name, value in params.items() + ) + + +def stop_step(lifecycle, uuid, status=None, status_details=None, exception=None, exception_type=None, traceback=None): + with lifecycle.update_step(uuid=uuid) as step_result: + if step_result is None: + return False + step_result.status = status or get_status(exception) + step_result.statusDetails = status_details or get_status_details(exception, exception_type, traceback) + lifecycle.stop_step(uuid=uuid) + return True + + +def start_gherkin_step(lifecycle, item, step, step_uuid=None): + if step_uuid is None: + step_uuid = get_step_uuid(step) + + start_step( + lifecycle, + step_uuid=step_uuid, + title=get_step_name(step), + parent_uuid=get_uuid(item.nodeid), + ) + + +def stop_gherkin_step(lifecycle, item, step_uuid, **kwargs): + res = stop_step(lifecycle, step_uuid, **kwargs) + if res: + save_reported_step(item, step_uuid) + return res + + +def ensure_gherkin_step_reported(lifecycle, item, step, step_uuid=None, **kwargs): + + if not step_uuid: + step_uuid = get_step_uuid(step) + + if stop_gherkin_step(lifecycle, item, step_uuid, **kwargs): + return + + start_gherkin_step(lifecycle, item, step, step_uuid) + stop_gherkin_step(lifecycle, item, step_uuid, **kwargs) + + +def report_undefined_step(lifecycle, item, step, exception): + ensure_gherkin_step_reported( + lifecycle, + item, + step, + status=Status.BROKEN, + status_details=StatusDetails( + message=format_exception(type(exception), exception), + ), + ) + + +def report_remaining_steps(lifecycle, item): + test_data = get_test_data(item) + scenario = test_data.scenario + excinfo = test_data.excinfo + reported_steps = test_data.reported_steps + + for step in scenario.steps: + step_uuid = get_step_uuid(step) + if step_uuid not in reported_steps: + __report_remaining_step(lifecycle, item, step, step_uuid, excinfo) + excinfo = None # Only show the full message and traceback once + + +def __report_remaining_step(lifecycle, item, step, step_uuid, excinfo): + args = [lifecycle, item, step, step_uuid] + kwargs = { + "exception": excinfo.value, + "exception_type": excinfo.type, + "traceback": excinfo.tb, + } if __is_step_running(lifecycle, step_uuid) and excinfo else { "status": Status.SKIPPED } + + ensure_gherkin_step_reported(*args, **kwargs) + + +def __is_step_running(lifecycle, step_uuid): + with lifecycle.update_step(uuid=step_uuid) as step_result: + return step_result is not None diff --git a/allure-pytest-bdd/src/storage.py b/allure-pytest-bdd/src/storage.py new file mode 100644 index 00000000..a9a291f3 --- /dev/null +++ b/allure-pytest-bdd/src/storage.py @@ -0,0 +1,29 @@ +import pytest +from .types import AllurePytestBddTestData + + +ALLURE_PYTEST_BDD_HASHKEY = pytest.StashKey() + + +def save_test_data(item, feature, scenario, pytest_params): + item.stash[ALLURE_PYTEST_BDD_HASHKEY] = AllurePytestBddTestData( + feature=feature, + scenario=scenario, + pytest_params=pytest_params, + ) + + +def save_excinfo(item, excinfo): + test_data = get_test_data(item) + if test_data: + test_data.excinfo = excinfo + + +def save_reported_step(item, step_uuid): + test_data = get_test_data(item) + if test_data: + test_data.reported_steps.add(step_uuid) + + +def get_test_data(item): + return item.stash.get(ALLURE_PYTEST_BDD_HASHKEY, (None, None)) diff --git a/allure-pytest-bdd/src/types.py b/allure-pytest-bdd/src/types.py index 4dccb1b6..2f0179ca 100644 --- a/allure-pytest-bdd/src/types.py +++ b/allure-pytest-bdd/src/types.py @@ -1,7 +1,11 @@ from collections import namedtuple -AllurePytestBddTestData = namedtuple( - "AllurePytestBddTestData", - ["feature", "scenario", "pytest_params"], -) +class AllurePytestBddTestData: + + def __init__(self, feature, scenario, pytest_params): + self.feature = feature + self.scenario = scenario + self.pytest_params = pytest_params + self.excinfo = None + self.reported_steps = set() diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index 5ec63ddd..3b9f3e43 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -2,6 +2,7 @@ import pytest from urllib.parse import urlparse from uuid import UUID + from allure_commons.model2 import Label from allure_commons.model2 import Link from allure_commons.model2 import StatusDetails @@ -15,15 +16,8 @@ from allure_commons.utils import represent from allure_commons.utils import SafeFormatter -from .types import AllurePytestBddTestData - -ALLURE_PYTEST_BDD_HASHKEY = pytest.StashKey() - -ALLURE_DESCRIPTION_MARK = "allure_description" -ALLURE_DESCRIPTION_HTML_MARK = "allure_description_html" -ALLURE_TITLE_MARK = "allure_title" -ALLURE_LABEL_MARK = 'allure_label' -ALLURE_LINK_MARK = 'allure_link' +from .storage import get_test_data +from . import api MARK_NAMES_TO_IGNORE = { "usefixtures", @@ -35,73 +29,33 @@ } -def save_test_data(item, feature, scenario, pytest_params): - item.stash[ALLURE_PYTEST_BDD_HASHKEY] = AllurePytestBddTestData( - feature=feature, - scenario=scenario, - pytest_params=pytest_params, - ) - - -def get_test_data(item): - return item.stash.get(ALLURE_PYTEST_BDD_HASHKEY, (None, None)) - - def get_marker_value(item, keyword): marker = item.get_closest_marker(keyword) return marker.args[0] if marker and marker.args else None -def get_allure_title(item): - return get_marker_value(item, ALLURE_TITLE_MARK) - def interpolate_args(format_str, args): return SafeFormatter().format(format_str, **args) if args else format_str -def get_allure_description(item, feature, scenario): - value = get_marker_value(item, ALLURE_DESCRIPTION_MARK) - if value: - return value - - feature_description = resolve_description(feature.description) - scenario_description = resolve_description(scenario.description) - return "\n\n".join(filter(None, [feature_description, scenario_description])) - - -def get_allure_description_html(item): - return get_marker_value(item, ALLURE_DESCRIPTION_HTML_MARK) - - -def iter_all_labels(item): - for mark in item.iter_markers(name=ALLURE_LABEL_MARK): - name = mark.kwargs.get("label_type") - if name: - yield from ((name, value) for value in mark.args or []) - - 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 iter_pytest_tags(item: pytest.Function): +def iter_pytest_tags(item): for mark in item.iter_markers(): if should_convert_mark_to_tag(mark): yield LabelType.TAG, mark.name -def iter_label_values(item, name): - return (pair for pair in iter_all_labels(item) if pair[0] == name) - - def convert_labels(labels): return [Label(name, value) for name, value in labels] def get_allure_labels(item): - return convert_labels(iter_all_labels(item)) + return convert_labels(api.iter_all_labels(item)) def get_link_patterns(config): @@ -130,19 +84,12 @@ def apply_link_pattern(patterns, link_type, url): return url if pattern is None else pattern.format(url) -def iter_all_links(item): - for marker in item.iter_markers(name=ALLURE_LINK_MARK): - url = marker.args[0] if marker and marker.args else None - if url: - yield url, marker.kwargs.get("name"), marker.kwargs.get("link_type") - - def convert_links(links): return [Link(url=url, name=name, type=link_type) for url, name, link_type in links] def get_allure_links(item): - return convert_links(iter_all_links(item)) + return convert_links(api.iter_all_links(item)) def resolve_description(description): @@ -159,12 +106,8 @@ def resolve_description(description): return "\n".join(description) or None -def get_step_name(step): - return f"{step.keyword} {step.name}" - - def get_test_name(node, scenario, params): - allure_name = get_allure_title(node) + allure_name = api.get_allure_title(node) if allure_name: return interpolate_args(allure_name, params) @@ -186,24 +129,20 @@ def get_uuid(*args): def get_status(exception): if exception: - if isinstance(exception, (AssertionError, pytest.fail.Exception)): - return Status.FAILED - elif isinstance(exception, pytest.skip.Exception): + if isinstance(exception, (pytest.skip.Exception, pytest.xfail.Exception)): return Status.SKIPPED + elif isinstance(exception, (AssertionError, pytest.fail.Exception)): + return Status.FAILED return Status.BROKEN else: return Status.PASSED -def get_status_details(exception): - message = str(exception) - trace = format_exception(type(exception), exception) - return StatusDetails(message=message, trace=trace) if message or trace else None - - -def get_status_details_for_step(exception_type, exception, exception_traceback): +def get_status_details(exception, exception_type=None, traceback=None): + if exception_type is None and exception is not None: + exception_type = type(exception) message = format_exception(exception_type, exception) - trace = format_traceback(exception_traceback) + trace = format_traceback(traceback or getattr(exception, "__traceback__", None)) return StatusDetails(message=message, trace=trace) if message or trace else None diff --git a/allure-python-commons-test/src/result.py b/allure-python-commons-test/src/result.py index 7a740cb3..9206e61d 100644 --- a/allure-python-commons-test/src/result.py +++ b/allure-python-commons-test/src/result.py @@ -66,6 +66,7 @@ from hamcrest import equal_to, none, not_none from hamcrest import has_entry, has_item from hamcrest import contains_string +from hamcrest import contains_exactly from allure_commons_test.lookup import maps_to @@ -93,6 +94,13 @@ def has_step(name, *matchers): ) +def with_steps(*matchers): + return has_entry( + "steps", + contains_exactly(*matchers), + ) + + def get_parameter_matcher(name, *matchers): return has_entry( 'parameters', diff --git a/tests/allure_pytest_bdd/acceptance/steps/__init__.py b/tests/allure_pytest_bdd/acceptance/steps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/allure_pytest_bdd/acceptance/steps/api_steps_test.py b/tests/allure_pytest_bdd/acceptance/steps/api_steps_test.py new file mode 100644 index 00000000..85bd6b8e --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/steps/api_steps_test.py @@ -0,0 +1,380 @@ +from hamcrest import assert_that +from hamcrest import all_of + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_title +from allure_commons_test.result import has_step +from allure_commons_test.result import with_steps +from allure_commons_test.result import with_status +from allure_commons_test.result import has_parameter +from allure_commons_test.result import has_status_details +from allure_commons_test.result import with_message_contains +from allure_commons_test.result import with_trace_contains + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_one_context_substep(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given substep + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("substep") + def given_substep(): + with allure.step("foo"): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given substep", + has_step( + "foo", + with_status("passed"), + ), + ), + ), + ) + + +def test_one_function_substep(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given substep + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.step("foo") + def fn(): + pass + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("substep") + def given_substep(): + fn() + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given substep", + has_step( + "foo", + with_status("passed"), + ), + ), + ), + ) + + +def test_nested_substeps(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given substeps + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @allure.step("foo") + def fn(): + pass + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("substeps") + def given_substeps(): + with allure.step("1"): + with allure.step("1.1"): + pass + with allure.step("1.2"): + pass + with allure.step("2"): + with allure.step("2.1"): + pass + with allure.step("2.2"): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given substeps", + with_steps( + all_of( + has_title("1"), + with_status("passed"), + with_steps( + all_of( + has_title("1.1"), + with_status("passed"), + ), + all_of( + has_title("1.2"), + with_status("passed"), + ), + ), + ), + all_of( + has_title("2"), + with_status("passed"), + with_steps( + all_of( + has_title("2.1"), + with_status("passed"), + ), + all_of( + has_title("2.2"), + with_status("passed"), + ), + ), + ), + ), + ), + ), + ) + + +def test_substep_with_parameters(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + step = allure.step("foo") + step.params = {"foo": "bar", "baz": {"qux": "qut"}} + with step: + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given noop", + has_step( + "foo", + with_status("passed"), + has_parameter("foo", "'bar'"), + has_parameter("baz", "{'qux': 'qut'}"), + ), + ), + ), + ) + + +def test_failed_substep(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given fail + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("fail") + def given_fail(): + with allure.step("foo"): + assert False + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given fail", + has_step( + "foo", + with_status("failed"), + has_status_details( + with_message_contains("AssertionError: assert False"), + with_trace_contains("in given_fail"), + ), + ), + ), + ), + ) + + +def test_broken_substep(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given break + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("break") + def given_break(): + with allure.step("foo"): + raise ValueError("Lorem Ipsum") + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given break", + has_step( + "foo", + with_status("broken"), + has_status_details( + with_message_contains("ValueError: Lorem Ipsum"), + with_trace_contains("in given_break"), + ), + ), + ), + ), + ) + + +def test_skipped_substep(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given skip + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("skip") + def given_skip(): + with allure.step("foo"): + pytest.skip("Lorem Ipsum") + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given skip", + has_step( + "foo", + with_status("skipped"), + has_status_details( + with_message_contains("Skipped: Lorem Ipsum"), + with_trace_contains("in given_skip"), + ), + ), + ), + ), + ) diff --git a/tests/allure_pytest_bdd/acceptance/steps/gherkin_steps_test.py b/tests/allure_pytest_bdd/acceptance/steps/gherkin_steps_test.py new file mode 100644 index 00000000..43c3451a --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/steps/gherkin_steps_test.py @@ -0,0 +1,475 @@ +from hamcrest import assert_that +from hamcrest import not_ +from hamcrest import all_of + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_title +from allure_commons_test.result import has_step +from allure_commons_test.result import with_steps +from allure_commons_test.result import with_status +from allure_commons_test.result import has_parameter +from allure_commons_test.result import has_status_details +from allure_commons_test.result import with_message_contains +from allure_commons_test.result import with_trace_contains + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_passed_step(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given pass + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("pass") + def given_pass(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given pass", + with_status("passed"), + not_(has_status_details()), + ), + ), + ) + + +def test_failed_step(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given fail + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("fail") + def given_fail(): + assert False + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given fail", + with_status("failed"), + has_status_details( + with_message_contains("AssertionError: assert False"), + with_trace_contains("in given_fail"), + ), + ), + ), + ) + + +def test_broken_step(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given break + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("break") + def given_break(): + raise ValueError("Lorem Ipsum") + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given break", + with_status("broken"), + has_status_details( + with_message_contains("ValueError: Lorem Ipsum"), + with_trace_contains("in given_break"), + ), + ), + ), + ) + + +def test_skipped_step(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given skip + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("skip") + def given_skip(): + pytest.skip("Lorem Ipsum") + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given skip", + with_status("skipped"), + has_status_details( + with_message_contains("Skipped: Lorem Ipsum"), + with_trace_contains("in given_skip"), + ), + ), + ), + ) + +def test_xfailed_step(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given xfail + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("xfail") + def given_xfail(): + pytest.xfail("Lorem Ipsum") + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given xfail", + with_status("skipped"), + has_status_details( + with_message_contains("XFailed: Lorem Ipsum"), + with_trace_contains("in given_xfail"), + ), + ), + ), + ) + + +def test_remaining_steps_are_reported_after_failed(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given fail + When skip + Then skip + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given, when, then + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("fail") + def given_fail(): + assert False + + @when("skip") + def when_skip(): + pass + + @then("skip") + def then_skip(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_steps( + has_title("Given fail"), + all_of( + has_title("When skip"), + with_status("skipped"), + not_(has_status_details()), + ), + all_of( + has_title("Then skip"), + with_status("skipped"), + not_(has_status_details()), + ), + ), + ), + ) + + +def test_remaining_steps_are_reported_after_skipped(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given skip + When skip + Then skip + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given, when, then + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("skip") + def given_skip(): + pytest.skip("Lorem Ipsum") + + @when("skip") + def when_skip(): + pass + + @then("skip") + def then_skip(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_steps( + has_title("Given skip"), + all_of( + has_title("When skip"), + with_status("skipped"), + not_(has_status_details()), + ), + all_of( + has_title("Then skip"), + with_status("skipped"), + not_(has_status_details()), + ), + ), + ), + ) + + +def test_remaining_steps_are_reported_after_xfailed(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given xfail + When skip + Then skip + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given, when, then + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("xfail") + def given_xfail(): + pytest.xfail("Lorem Ipsum") + + @when("skip") + def when_skip(): + pass + + @then("skip") + def then_skip(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_steps( + has_title("Given xfail"), + all_of( + has_title("When skip"), + with_status("skipped"), + not_(has_status_details()), + ), + all_of( + has_title("Then skip"), + with_status("skipped"), + not_(has_status_details()), + ), + ), + ), + ) + + +def test_undefined_step(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given unknown + When skip + Then skip + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given, when, then + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @when("skip") + def when_skip(): + pass + + @then("skip") + def then_skip(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + cli_args=["--capture=no"] + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_steps( + all_of( + has_title("Given unknown"), + with_status("broken"), + has_status_details( + with_message_contains("Step definition is not found: Given \"unknown\""), + ), + ), + all_of( + has_title("When skip"), + with_status("skipped"), + not_(has_status_details()), + ), + all_of( + has_title("Then skip"), + with_status("skipped"), + not_(has_status_details()), + ), + ), + ), + ) diff --git a/tests/allure_pytest_bdd/acceptance/steps_test.py b/tests/allure_pytest_bdd/acceptance/steps_test.py deleted file mode 100644 index f80c2108..00000000 --- a/tests/allure_pytest_bdd/acceptance/steps_test.py +++ /dev/null @@ -1,198 +0,0 @@ -from hamcrest import assert_that - -from allure_commons_test.report import has_test_case -from allure_commons_test.result import has_step -from allure_commons_test.result import with_status -from allure_commons_test.result import has_parameter -from allure_commons_test.result import has_status_details -from allure_commons_test.result import with_message_contains -from allure_commons_test.result import with_trace_contains - -from tests.allure_pytest.pytest_runner import AllurePytestRunner - - -def test_one_context_substep(allure_pytest_bdd_runner: AllurePytestRunner): - feature_content = ( - """ - Feature: Foo - Scenario: Bar - Given noop - """ - ) - steps_content = ( - """ - from pytest_bdd import scenario, given - import allure - - @scenario("sample.feature", "Bar") - def test_scenario(): - pass - - @given("noop") - def given_noop(): - with allure.step("foo"): - pass - """ - ) - - allure_results = allure_pytest_bdd_runner.run_pytest( - ("sample.feature", feature_content), - steps_content, - ) - - assert_that( - allure_results, - has_test_case( - "sample.feature:Bar", - has_step( - "Given noop", - has_step( - "foo", - with_status("passed"), - ), - ), - ), - ) - - -def test_one_function_substep(allure_pytest_bdd_runner: AllurePytestRunner): - feature_content = ( - """ - Feature: Foo - Scenario: Bar - Given noop - """ - ) - steps_content = ( - """ - from pytest_bdd import scenario, given - import allure - - @allure.step("foo") - def fn(): - pass - - @scenario("sample.feature", "Bar") - def test_scenario(): - pass - - @given("noop") - def given_noop(): - fn() - """ - ) - - allure_results = allure_pytest_bdd_runner.run_pytest( - ("sample.feature", feature_content), - steps_content, - ) - - assert_that( - allure_results, - has_test_case( - "sample.feature:Bar", - has_step( - "Given noop", - has_step( - "foo", - with_status("passed"), - ), - ), - ), - ) - - -def test_substep_with_parameters(allure_pytest_bdd_runner: AllurePytestRunner): - feature_content = ( - """ - Feature: Foo - Scenario: Bar - Given noop - """ - ) - steps_content = ( - """ - from pytest_bdd import scenario, given - import allure - - @scenario("sample.feature", "Bar") - def test_scenario(): - pass - - @given("noop") - def given_noop(): - step = allure.step("foo") - step.params = {"foo": "bar", "baz": {"qux": "qut"}} - with step: - pass - """ - ) - - allure_results = allure_pytest_bdd_runner.run_pytest( - ("sample.feature", feature_content), - steps_content, - ) - - assert_that( - allure_results, - has_test_case( - "sample.feature:Bar", - has_step( - "Given noop", - has_step( - "foo", - with_status("passed"), - has_parameter("foo", "'bar'"), - has_parameter("baz", "{'qux': 'qut'}"), - ), - ), - ), - ) - - -def test_failed_substep(allure_pytest_bdd_runner: AllurePytestRunner): - feature_content = ( - """ - Feature: Foo - Scenario: Bar - Given noop - """ - ) - steps_content = ( - """ - from pytest_bdd import scenario, given - import allure - - @scenario("sample.feature", "Bar") - def test_scenario(): - pass - - @given("noop") - def given_noop(): - with allure.step("foo"): - assert False - """ - ) - - allure_results = allure_pytest_bdd_runner.run_pytest( - ("sample.feature", feature_content), - steps_content, - ) - - assert_that( - allure_results, - has_test_case( - "sample.feature:Bar", - has_step( - "Given noop", - has_step( - "foo", - with_status("failed"), - has_status_details( - with_message_contains("AssertionError: assert False"), - with_trace_contains("in given_noop"), - ), - ), - ), - ), - ) From c69b5a9bea500b9a1cd0d01ac0474deaa3137e29 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 19 Mar 2025 16:50:11 +0700 Subject: [PATCH 15/26] refactor(pytest-bdd): merge utils --- allure-pytest-bdd/src/{api.py => hooks.py} | 55 ++----------- allure-pytest-bdd/src/plugin.py | 15 ++-- allure-pytest-bdd/src/pytest_bdd_listener.py | 7 +- allure-pytest-bdd/src/utils.py | 81 ++++++++++++++----- .../acceptance/labels/features_test.py | 1 - .../acceptance/steps/gherkin_steps_test.py | 1 - 6 files changed, 80 insertions(+), 80 deletions(-) rename allure-pytest-bdd/src/{api.py => hooks.py} (71%) diff --git a/allure-pytest-bdd/src/api.py b/allure-pytest-bdd/src/hooks.py similarity index 71% rename from allure-pytest-bdd/src/api.py rename to allure-pytest-bdd/src/hooks.py index 2215ceb0..19d2c0c0 100644 --- a/allure-pytest-bdd/src/api.py +++ b/allure-pytest-bdd/src/hooks.py @@ -7,22 +7,19 @@ from allure_commons.model2 import Parameter from allure_commons.utils import represent -from .utils import apply_link_pattern +from .utils import ALLURE_DESCRIPTION_HTML_MARK +from .utils import ALLURE_DESCRIPTION_MARK +from .utils import ALLURE_LABEL_MARK +from .utils import ALLURE_LINK_MARK +from .utils import ALLURE_TITLE_MARK + from .utils import get_link_patterns -from .utils import get_marker_value -from .utils import resolve_description +from .utils import apply_link_pattern from .steps import start_step from .steps import stop_step -ALLURE_DESCRIPTION_MARK = "allure_description" -ALLURE_DESCRIPTION_HTML_MARK = "allure_description_html" -ALLURE_TITLE_MARK = "allure_title" -ALLURE_LABEL_MARK = 'allure_label' -ALLURE_LINK_MARK = 'allure_link' - - -class AllurePytestBddApi: +class AllurePytestBddApiHooks: def __init__(self, config, lifecycle): self.lifecycle = lifecycle self.__link_patterns = get_link_patterns(config) @@ -104,39 +101,3 @@ def stop_step(self, uuid, exc_type, exc_val, exc_tb): exception_type=exc_type, traceback=exc_tb, ) - - -def get_allure_title(item): - return get_marker_value(item, ALLURE_TITLE_MARK) - - -def get_allure_description(item, feature, scenario): - value = get_marker_value(item, ALLURE_DESCRIPTION_MARK) - if value: - return value - - feature_description = resolve_description(feature.description) - scenario_description = resolve_description(scenario.description) - return "\n\n".join(filter(None, [feature_description, scenario_description])) - - -def get_allure_description_html(item): - return get_marker_value(item, ALLURE_DESCRIPTION_HTML_MARK) - - -def iter_all_labels(item): - for mark in item.iter_markers(name=ALLURE_LABEL_MARK): - name = mark.kwargs.get("label_type") - if name: - yield from ((name, value) for value in mark.args or []) - - -def iter_label_values(item, name): - return (pair for pair in iter_all_labels(item) if pair[0] == name) - - -def iter_all_links(item): - for marker in item.iter_markers(name=ALLURE_LINK_MARK): - url = marker.args[0] if marker and marker.args else None - if url: - yield url, marker.kwargs.get("name"), marker.kwargs.get("link_type") diff --git a/allure-pytest-bdd/src/plugin.py b/allure-pytest-bdd/src/plugin.py index b8a6d798..51b4a870 100644 --- a/allure-pytest-bdd/src/plugin.py +++ b/allure-pytest-bdd/src/plugin.py @@ -5,13 +5,14 @@ from allure_commons.logger import AllureFileLogger from allure_commons.lifecycle import AllureLifecycle -from .api import AllurePytestBddApi +from .hooks import AllurePytestBddApiHooks from .pytest_bdd_listener import PytestBDDListener -from .api import ALLURE_TITLE_MARK -from .api import ALLURE_DESCRIPTION_MARK -from .api import ALLURE_DESCRIPTION_HTML_MARK -from .api import ALLURE_LABEL_MARK -from .api import ALLURE_LINK_MARK + +from .utils import ALLURE_TITLE_MARK +from .utils import ALLURE_DESCRIPTION_MARK +from .utils import ALLURE_DESCRIPTION_HTML_MARK +from .utils import ALLURE_LABEL_MARK +from .utils import ALLURE_LINK_MARK def pytest_addoption(parser): @@ -80,7 +81,7 @@ def pytest_configure(config): allure_commons.plugin_manager.register(pytest_bdd_listener) config.add_cleanup(cleanup_factory(pytest_bdd_listener)) - allure_api_impl = AllurePytestBddApi(config, lifecycle) + allure_api_impl = AllurePytestBddApiHooks(config, lifecycle) allure_commons.plugin_manager.register(allure_api_impl) config.add_cleanup(cleanup_factory(allure_api_impl)) diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index 51d2f0b0..0f4a5e14 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -11,8 +11,6 @@ from allure_commons.utils import host_tag, thread_tag from allure_commons.utils import md5 -from .api import get_allure_description -from .api import get_allure_description_html from .steps import get_step_uuid from .steps import report_remaining_steps from .steps import report_undefined_step @@ -20,9 +18,11 @@ from .steps import stop_gherkin_step from .storage import save_excinfo from .storage import save_test_data -from .utils import convert_params +from .utils import get_allure_description +from .utils import get_allure_description_html from .utils import get_allure_labels from .utils import get_allure_links +from .utils import convert_params from .utils import get_full_name from .utils import get_outline_params from .utils import get_pytest_params @@ -116,7 +116,6 @@ def pytest_runtest_makereport(self, item, call): test_result.statusDetails = status_details if report.when == "call" and test_result: - print("in makereport", excinfo) save_excinfo(item, excinfo) if test_result.status not in [Status.PASSED, Status.FAILED]: test_result.status = status diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index 3b9f3e43..2125ba23 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -1,8 +1,9 @@ import os -import pytest from urllib.parse import urlparse from uuid import UUID +import pytest + from allure_commons.model2 import Label from allure_commons.model2 import Link from allure_commons.model2 import StatusDetails @@ -10,6 +11,7 @@ from allure_commons.model2 import Parameter from allure_commons.types import LabelType from allure_commons.types import LinkType + from allure_commons.utils import format_exception from allure_commons.utils import format_traceback from allure_commons.utils import md5 @@ -17,7 +19,12 @@ from allure_commons.utils import SafeFormatter from .storage import get_test_data -from . import api + +ALLURE_DESCRIPTION_MARK = "allure_description" +ALLURE_DESCRIPTION_HTML_MARK = "allure_description_html" +ALLURE_TITLE_MARK = "allure_title" +ALLURE_LABEL_MARK = 'allure_label' +ALLURE_LINK_MARK = 'allure_link' MARK_NAMES_TO_IGNORE = { "usefixtures", @@ -28,26 +35,33 @@ "parametrize", } +def get_allure_title(item): + return get_marker_value(item, ALLURE_TITLE_MARK) -def get_marker_value(item, keyword): - marker = item.get_closest_marker(keyword) - return marker.args[0] if marker and marker.args else None +def get_allure_description(item, feature, scenario): + value = get_marker_value(item, ALLURE_DESCRIPTION_MARK) + if value: + return value + feature_description = resolve_description(feature.description) + scenario_description = resolve_description(scenario.description) + return "\n\n".join(filter(None, [feature_description, scenario_description])) -def interpolate_args(format_str, args): - return SafeFormatter().format(format_str, **args) if args else format_str +def get_allure_description_html(item): + return get_marker_value(item, ALLURE_DESCRIPTION_HTML_MARK) -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 iter_all_labels(item): + for mark in item.iter_markers(name=ALLURE_LABEL_MARK): + name = mark.kwargs.get("label_type") + if name: + yield from ((name, value) for value in mark.args or []) -def iter_pytest_tags(item): - for mark in item.iter_markers(): - if should_convert_mark_to_tag(mark): - yield LabelType.TAG, mark.name + +def iter_label_values(item, name): + return (pair for pair in iter_all_labels(item) if pair[0] == name) def convert_labels(labels): @@ -55,7 +69,22 @@ def convert_labels(labels): def get_allure_labels(item): - return convert_labels(api.iter_all_labels(item)) + return convert_labels(iter_all_labels(item)) + + +def iter_all_links(item): + for marker in item.iter_markers(name=ALLURE_LINK_MARK): + url = marker.args[0] if marker and marker.args else None + if url: + yield url, marker.kwargs.get("name"), marker.kwargs.get("link_type") + + +def convert_links(links): + return [Link(url=url, name=name, type=link_type) for url, name, link_type in links] + + +def get_allure_links(item): + return convert_links(iter_all_links(item)) def get_link_patterns(config): @@ -84,12 +113,24 @@ def apply_link_pattern(patterns, link_type, url): return url if pattern is None else pattern.format(url) -def convert_links(links): - return [Link(url=url, name=name, type=link_type) for url, name, link_type in links] +def get_marker_value(item, keyword): + marker = item.get_closest_marker(keyword) + return marker.args[0] if marker and marker.args else None -def get_allure_links(item): - return convert_links(api.iter_all_links(item)) +def interpolate_args(format_str, args): + return SafeFormatter().format(format_str, **args) if args else format_str + + +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 iter_pytest_tags(item): + for mark in item.iter_markers(): + if should_convert_mark_to_tag(mark): + yield LabelType.TAG, mark.name def resolve_description(description): @@ -107,7 +148,7 @@ def resolve_description(description): def get_test_name(node, scenario, params): - allure_name = api.get_allure_title(node) + allure_name = get_allure_title(node) if allure_name: return interpolate_args(allure_name, params) diff --git a/tests/allure_pytest_bdd/acceptance/labels/features_test.py b/tests/allure_pytest_bdd/acceptance/labels/features_test.py index c8def23a..554f6fbc 100644 --- a/tests/allure_pytest_bdd/acceptance/labels/features_test.py +++ b/tests/allure_pytest_bdd/acceptance/labels/features_test.py @@ -75,7 +75,6 @@ def given_noop(): allure_results = allure_pytest_bdd_runner.run_pytest( ("sample.feature", feature_content), steps_content, - cli_args=["--capture=no"] ) assert_that( diff --git a/tests/allure_pytest_bdd/acceptance/steps/gherkin_steps_test.py b/tests/allure_pytest_bdd/acceptance/steps/gherkin_steps_test.py index 43c3451a..271380dd 100644 --- a/tests/allure_pytest_bdd/acceptance/steps/gherkin_steps_test.py +++ b/tests/allure_pytest_bdd/acceptance/steps/gherkin_steps_test.py @@ -445,7 +445,6 @@ def then_skip(): allure_results = allure_pytest_bdd_runner.run_pytest( ("sample.feature", feature_content), steps_content, - cli_args=["--capture=no"] ) assert_that( From e139391bdaee573bd0839708913717c914aa17da Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 19 Mar 2025 21:35:31 +0700 Subject: [PATCH 16/26] refactor(pytest-bdd): mode attach hooks to listener module --- .../src/{hooks.py => allure_api_listener.py} | 16 ++++++++++++-- allure-pytest-bdd/src/plugin.py | 2 +- allure-pytest-bdd/src/pytest_bdd_listener.py | 17 ++++----------- allure-pytest-bdd/src/utils.py | 21 +++++++++++++++++++ 4 files changed, 40 insertions(+), 16 deletions(-) rename allure-pytest-bdd/src/{hooks.py => allure_api_listener.py} (87%) diff --git a/allure-pytest-bdd/src/hooks.py b/allure-pytest-bdd/src/allure_api_listener.py similarity index 87% rename from allure-pytest-bdd/src/hooks.py rename to allure-pytest-bdd/src/allure_api_listener.py index 19d2c0c0..1a1f532b 100644 --- a/allure-pytest-bdd/src/hooks.py +++ b/allure-pytest-bdd/src/allure_api_listener.py @@ -13,8 +13,10 @@ from .utils import ALLURE_LINK_MARK from .utils import ALLURE_TITLE_MARK -from .utils import get_link_patterns from .utils import apply_link_pattern +from .utils import attach_data +from .utils import attach_file +from .utils import get_link_patterns from .steps import start_step from .steps import stop_step @@ -62,7 +64,9 @@ def decorate_as_label(self, label_type, labels): @allure_commons.hookimpl def add_label(self, label_type, labels): with self.lifecycle.update_test_case() as test_result: - test_result.labels.extend(Label(name=label_type, value=value) for value in labels or []) + test_result.labels.extend( + Label(name=label_type, value=value) for value in labels or [] + ) @allure_commons.hookimpl def decorate_as_link(self, url, link_type, name): @@ -101,3 +105,11 @@ def stop_step(self, uuid, exc_type, exc_val, exc_tb): exception_type=exc_type, traceback=exc_tb, ) + + @allure_commons.hookimpl + def attach_data(self, body, name, attachment_type, extension): + attach_data(self.lifecycle, body, name, attachment_type, extension) + + @allure_commons.hookimpl + def attach_file(self, source, name, attachment_type, extension): + attach_file(self.lifecycle, source, name, attachment_type, extension) diff --git a/allure-pytest-bdd/src/plugin.py b/allure-pytest-bdd/src/plugin.py index 51b4a870..ae331e11 100644 --- a/allure-pytest-bdd/src/plugin.py +++ b/allure-pytest-bdd/src/plugin.py @@ -5,7 +5,7 @@ from allure_commons.logger import AllureFileLogger from allure_commons.lifecycle import AllureLifecycle -from .hooks import AllurePytestBddApiHooks +from .allure_api_listener import AllurePytestBddApiHooks from .pytest_bdd_listener import PytestBDDListener from .utils import ALLURE_TITLE_MARK diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index 0f4a5e14..33886c55 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -1,8 +1,6 @@ import pytest -import allure_commons from allure_commons.utils import now -from allure_commons.utils import uuid4 from allure_commons.model2 import Label from allure_commons.model2 import Status from allure_commons.model2 import StatusDetails @@ -18,6 +16,7 @@ from .steps import stop_gherkin_step from .storage import save_excinfo from .storage import save_test_data +from .utils import attach_data from .utils import get_allure_description from .utils import get_allure_description_html from .utils import get_allure_labels @@ -126,20 +125,12 @@ def pytest_runtest_makereport(self, item, call): test_result.status = status test_result.statusDetails = status_details if report.caplog: - self.attach_data(report.caplog, "log", AttachmentType.TEXT, None) + attach_data(self.lifecycle, report.caplog, "log", AttachmentType.TEXT, None) if report.capstdout: - self.attach_data(report.capstdout, "stdout", AttachmentType.TEXT, None) + attach_data(self.lifecycle, report.capstdout, "stdout", AttachmentType.TEXT, None) if report.capstderr: - self.attach_data(report.capstderr, "stderr", AttachmentType.TEXT, None) + attach_data(self.lifecycle, report.capstderr, "stderr", AttachmentType.TEXT, None) post_process_test_result(item, test_result) if report.when == 'teardown': self.lifecycle.write_test_case(uuid=uuid) - - @allure_commons.hookimpl - def attach_data(self, body, name, attachment_type, extension): - self.lifecycle.attach_data(uuid4(), body, name=name, attachment_type=attachment_type, extension=extension) - - @allure_commons.hookimpl - def attach_file(self, source, name, attachment_type, extension): - self.lifecycle.attach_file(uuid4(), source, name=name, attachment_type=attachment_type, extension=extension) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index 2125ba23..ae2ab958 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -17,6 +17,7 @@ from allure_commons.utils import md5 from allure_commons.utils import represent from allure_commons.utils import SafeFormatter +from allure_commons.utils import uuid4 from .storage import get_test_data @@ -260,3 +261,23 @@ def post_process_test_result(item, test_result): parameters=test_result.parameters, pytest_params=test_data.pytest_params, ) + + +def attach_data(lifecycle, body, name, attachment_type, extension): + lifecycle.attach_data( + uuid4(), + body, + name=name, + attachment_type=attachment_type, + extension=extension, + ) + + +def attach_file(lifecycle, source, name, attachment_type, extension): + lifecycle.attach_file( + uuid4(), + source, + name=name, + attachment_type=attachment_type, + extension=extension, + ) From 534d972ab5aa3624683424582025dbccf35f9b5f Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 19 Mar 2025 21:39:05 +0700 Subject: [PATCH 17/26] fix(pytest-bdd): better xfail and teardown support --- allure-pytest-bdd/src/pytest_bdd_listener.py | 18 +- allure-pytest-bdd/src/utils.py | 34 +- .../acceptance/outcomes_test.py | 765 ++++++++++++++++++ 3 files changed, 802 insertions(+), 15 deletions(-) create mode 100644 tests/allure_pytest_bdd/acceptance/outcomes_test.py diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index 33886c55..6540370a 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -3,7 +3,6 @@ from allure_commons.utils import now from allure_commons.model2 import Label from allure_commons.model2 import Status -from allure_commons.model2 import StatusDetails from allure_commons.types import LabelType, AttachmentType from allure_commons.utils import platform_label from allure_commons.utils import host_tag, thread_tag @@ -26,6 +25,7 @@ from .utils import get_outline_params from .utils import get_pytest_params from .utils import get_pytest_report_status +from .utils import get_scenario_status_details from .utils import get_test_name from .utils import get_uuid from .utils import post_process_test_result @@ -98,14 +98,10 @@ def pytest_bdd_step_func_lookup_error(self, request, feature, scenario, step, ex def pytest_runtest_makereport(self, item, call): report = (yield).get_result() - status = get_pytest_report_status(report) - excinfo = call.excinfo - status_details = StatusDetails( - message=excinfo.exconly(), - trace=report.longreprtext, - ) if excinfo else None + status = get_pytest_report_status(report, excinfo) + status_details = get_scenario_status_details(report, excinfo) uuid = get_uuid(report.nodeid) with self.lifecycle.update_test_case(uuid=uuid) as test_result: @@ -115,13 +111,17 @@ def pytest_runtest_makereport(self, item, call): test_result.statusDetails = status_details if report.when == "call" and test_result: + + # Save the exception to access it from the finalizer to report + # the remaining steps save_excinfo(item, excinfo) - if test_result.status not in [Status.PASSED, Status.FAILED]: + + if test_result.status is None or test_result.status == Status.PASSED: test_result.status = status test_result.statusDetails = status_details if report.when == "teardown" and test_result: - if test_result.status == Status.PASSED and status != Status.PASSED: + if test_result.status == Status.PASSED and status in [Status.FAILED, Status.BROKEN]: test_result.status = status test_result.statusDetails = status_details if report.caplog: diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index ae2ab958..85363889 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -188,12 +188,34 @@ def get_status_details(exception, exception_type=None, traceback=None): return StatusDetails(message=message, trace=trace) if message or trace else None -def get_pytest_report_status(pytest_report): - pytest_statuses = ('failed', 'passed', 'skipped') - statuses = (Status.FAILED, Status.PASSED, Status.SKIPPED) - for pytest_status, status in zip(pytest_statuses, statuses): - if getattr(pytest_report, pytest_status): - return status +def get_pytest_report_status(pytest_report, excinfo): + if pytest_report.failed: + return get_status(excinfo.value) if excinfo else Status.BROKEN + + if pytest_report.passed: + return Status.PASSED + + if pytest_report.skipped: + return Status.SKIPPED + + +def is_runtime_xfail(excinfo): + return isinstance(excinfo.value, pytest.xfail.Exception) + + +def get_scenario_status_details(report, excinfo): + if excinfo: + message = excinfo.exconly() + trace = report.longreprtext + if not is_runtime_xfail(excinfo) and hasattr(report, "wasxfail"): + reason = report.wasxfail + message = (f"XFAIL {reason}" if reason else "XFAIL") + "\n\n" + message + return StatusDetails(message=message, trace=trace) + elif report.passed and hasattr(report, "wasxfail"): + reason = report.wasxfail + return StatusDetails(message=f"XPASS {reason}" if reason else "XPASS") + elif report.failed and "XPASS(strict)" in report.longrepr: + return StatusDetails(message=report.longrepr) def get_outline_params(node): diff --git a/tests/allure_pytest_bdd/acceptance/outcomes_test.py b/tests/allure_pytest_bdd/acceptance/outcomes_test.py new file mode 100644 index 00000000..c425312f --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/outcomes_test.py @@ -0,0 +1,765 @@ +from hamcrest import assert_that +from hamcrest import not_ +from hamcrest import empty +from hamcrest import all_of +from hamcrest import has_entry +from hamcrest import anything + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import with_status +from allure_commons_test.result import has_status_details +from allure_commons_test.result import with_message_contains +from allure_commons_test.result import with_trace_contains + +from tests.allure_pytest.pytest_runner import AllurePytestRunner + + +def test_passed_scenario(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given pass + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("pass") + def given_pass(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("passed"), + not_(has_status_details()), + ), + ) + + +def test_scenario_fail_in_step(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given fail + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("fail") + def given_fail(): + assert False + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("failed"), + has_status_details( + with_message_contains("AssertionError: assert False"), + with_trace_contains("def given_fail():"), + ), + ), + ) + + +def test_scenario_fail_in_scenario(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + assert False + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("failed"), + has_status_details( + with_message_contains("AssertionError: assert False"), + with_trace_contains("def test_scenario():"), + ), + ), + ) + + +def test_scenario_break_in_step(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given break + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("break") + def given_break(): + raise ValueError("Lorem Ipsum") + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("broken"), + has_status_details( + with_message_contains("ValueError: Lorem Ipsum"), + with_trace_contains("def given_break():"), + ), + ), + ) + + +def test_scenario_break_in_scenario(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + raise ValueError("Lorem Ipsum") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("broken"), + has_status_details( + with_message_contains("ValueError: Lorem Ipsum"), + with_trace_contains("def test_scenario():"), + ), + ), + ) + + +def test_scenario_skip_in_step(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given skip + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("skip") + def given_skip(): + pytest.skip("Lorem Ipsum") + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("skipped"), + has_status_details( + with_message_contains("Skipped: Lorem Ipsum"), + with_trace_contains("test_scenario_skip_in_step.py"), + ), + ), + ) + + +def test_scenario_skip_in_scenario(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pytest.skip("Lorem Ipsum") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("skipped"), + has_status_details( + with_message_contains("Skipped: Lorem Ipsum"), + with_trace_contains("test_scenario_skip_in_scenario.py"), + ), + ), + ) + + +def test_scenario_skip_mark(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @pytest.mark.skip("Lorem Ipsum") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that(allure_results.test_cases, empty()) + + +def test_scenario_xfail_in_step(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given xfail + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("xfail") + def given_xfail(): + pytest.xfail("Lorem Ipsum") + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("skipped"), + has_status_details( + all_of( + with_message_contains("XFailed: Lorem Ipsum"), + not_(with_message_contains("XFAIL reason: Lorem Ipsum\n\n")), + ), + with_trace_contains("def given_xfail():"), + ), + ), + ) + + +def test_scenario_xfail_in_scenario(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pytest.xfail("Lorem Ipsum") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("skipped"), + has_status_details( + all_of( + with_message_contains("XFailed: Lorem Ipsum"), + not_(with_message_contains("XFAIL reason: Lorem Ipsum\n\n")), + ), + with_trace_contains("def test_scenario():"), + ), + ), + ) + + +def test_scenario_xfail_mark(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @pytest.mark.xfail(reason="Lorem Ipsum") + @scenario("sample.feature", "Bar") + def test_scenario(): + assert False + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("skipped"), + has_status_details( + with_message_contains("XFAIL Lorem Ipsum\n\nAssertionError: assert False"), + with_trace_contains("def test_scenario():"), + ), + ), + ) + + +def test_scenario_xfail_mark_passed(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @pytest.mark.xfail(reason="Lorem Ipsum") + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("passed"), + has_status_details( + with_message_contains("XPASS Lorem Ipsum"), + not_(has_entry("trace", anything())), + ), + ), + ) + +def test_scenario_xfail_mark_strict(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @pytest.mark.xfail(reason="Lorem Ipsum", strict=True) + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("broken"), + has_status_details( + with_message_contains("[XPASS(strict)] Lorem Ipsum"), + not_(has_entry("trace", anything())), + ), + ), + ) + + +def test_passed_setup_teardown(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @pytest.fixture + def setup(): + yield + + @scenario("sample.feature", "Bar") + def test_scenario(setup): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("passed"), + not_(has_status_details()), + ), + ) + + +def test_passed_teardown_not_overwrite_failed_status(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @pytest.fixture + def setup(): + yield + + @scenario("sample.feature", "Bar") + def test_scenario(setup): + assert False + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("failed"), + ), + ) + + +def test_failed_teardown_overwrite_passed_status(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @pytest.fixture + def setup(): + yield + assert False + + @scenario("sample.feature", "Bar") + def test_scenario(setup): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("failed"), + ), + ) + + +def test_broken_teardown_overwrite_passed_status(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @pytest.fixture + def setup(): + yield + raise ValueError("Lorem Ipsum") + + @scenario("sample.feature", "Bar") + def test_scenario(setup): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("broken"), + ), + ) + + +def test_skipped_teardown_not_overwrite_passed_status(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @pytest.fixture + def setup(): + yield + pytest.skip("Lorem Ipsum") + + @scenario("sample.feature", "Bar") + def test_scenario(setup): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_status("passed"), + ), + ) From 3fd3cec12ff0bfc164d8a5ff8b1094ab5a62abcd Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 19 Mar 2025 21:50:19 +0700 Subject: [PATCH 18/26] refactor(pytest-bdd): merge types module into storage --- allure-pytest-bdd/src/storage.py | 11 ++++++++++- allure-pytest-bdd/src/types.py | 11 ----------- 2 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 allure-pytest-bdd/src/types.py diff --git a/allure-pytest-bdd/src/storage.py b/allure-pytest-bdd/src/storage.py index a9a291f3..a2060c4b 100644 --- a/allure-pytest-bdd/src/storage.py +++ b/allure-pytest-bdd/src/storage.py @@ -1,10 +1,19 @@ import pytest -from .types import AllurePytestBddTestData ALLURE_PYTEST_BDD_HASHKEY = pytest.StashKey() +class AllurePytestBddTestData: + + def __init__(self, feature, scenario, pytest_params): + self.feature = feature + self.scenario = scenario + self.pytest_params = pytest_params + self.excinfo = None + self.reported_steps = set() + + def save_test_data(item, feature, scenario, pytest_params): item.stash[ALLURE_PYTEST_BDD_HASHKEY] = AllurePytestBddTestData( feature=feature, diff --git a/allure-pytest-bdd/src/types.py b/allure-pytest-bdd/src/types.py deleted file mode 100644 index 2f0179ca..00000000 --- a/allure-pytest-bdd/src/types.py +++ /dev/null @@ -1,11 +0,0 @@ -from collections import namedtuple - - -class AllurePytestBddTestData: - - def __init__(self, feature, scenario, pytest_params): - self.feature = feature - self.scenario = scenario - self.pytest_params = pytest_params - self.excinfo = None - self.reported_steps = set() From 9a0108742414a43d11c7f3529f3b1defeeefecf1 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 19 Mar 2025 21:50:36 +0700 Subject: [PATCH 19/26] refactor(pytest-bdd): linter --- allure-pytest-bdd/src/pytest_bdd_listener.py | 2 +- allure-pytest-bdd/src/steps.py | 6 +++--- allure-pytest-bdd/src/utils.py | 1 + tests/allure_pytest_bdd/acceptance/outcomes_test.py | 1 + .../acceptance/steps/gherkin_steps_test.py | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index 6540370a..4d3fbe00 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -45,7 +45,7 @@ def pytest_bdd_before_scenario(self, request, feature, scenario): uuid = get_uuid(item.nodeid) outline_params = get_outline_params(item) pytest_params = get_pytest_params(item) - params = { **pytest_params, **outline_params } + params = {**pytest_params, **outline_params} save_test_data( item=item, feature=feature, diff --git a/allure-pytest-bdd/src/steps.py b/allure-pytest-bdd/src/steps.py index 542501d5..cb0f5c49 100644 --- a/allure-pytest-bdd/src/steps.py +++ b/allure-pytest-bdd/src/steps.py @@ -20,7 +20,7 @@ def get_step_uuid(step): def start_step(lifecycle, step_uuid, title, params=None, parent_uuid=None): - with lifecycle.start_step(uuid=step_uuid, parent_uuid=parent_uuid) as step_result: + with lifecycle.start_step(uuid=step_uuid, parent_uuid=parent_uuid) as step_result: step_result.name = title if params: step_result.parameters.extend( @@ -94,7 +94,7 @@ def report_remaining_steps(lifecycle, item): step_uuid = get_step_uuid(step) if step_uuid not in reported_steps: __report_remaining_step(lifecycle, item, step, step_uuid, excinfo) - excinfo = None # Only show the full message and traceback once + excinfo = None # Only show the full message and traceback once def __report_remaining_step(lifecycle, item, step, step_uuid, excinfo): @@ -103,7 +103,7 @@ def __report_remaining_step(lifecycle, item, step, step_uuid, excinfo): "exception": excinfo.value, "exception_type": excinfo.type, "traceback": excinfo.tb, - } if __is_step_running(lifecycle, step_uuid) and excinfo else { "status": Status.SKIPPED } + } if __is_step_running(lifecycle, step_uuid) and excinfo else {"status": Status.SKIPPED} ensure_gherkin_step_reported(*args, **kwargs) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index 85363889..f5133d5c 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -36,6 +36,7 @@ "parametrize", } + def get_allure_title(item): return get_marker_value(item, ALLURE_TITLE_MARK) diff --git a/tests/allure_pytest_bdd/acceptance/outcomes_test.py b/tests/allure_pytest_bdd/acceptance/outcomes_test.py index c425312f..14394817 100644 --- a/tests/allure_pytest_bdd/acceptance/outcomes_test.py +++ b/tests/allure_pytest_bdd/acceptance/outcomes_test.py @@ -508,6 +508,7 @@ def given_noop(): ), ) + def test_scenario_xfail_mark_strict(allure_pytest_bdd_runner: AllurePytestRunner): feature_content = ( """ diff --git a/tests/allure_pytest_bdd/acceptance/steps/gherkin_steps_test.py b/tests/allure_pytest_bdd/acceptance/steps/gherkin_steps_test.py index 271380dd..d7c7fc1a 100644 --- a/tests/allure_pytest_bdd/acceptance/steps/gherkin_steps_test.py +++ b/tests/allure_pytest_bdd/acceptance/steps/gherkin_steps_test.py @@ -7,7 +7,6 @@ from allure_commons_test.result import has_step from allure_commons_test.result import with_steps from allure_commons_test.result import with_status -from allure_commons_test.result import has_parameter from allure_commons_test.result import has_status_details from allure_commons_test.result import with_message_contains from allure_commons_test.result import with_trace_contains @@ -188,6 +187,7 @@ def given_skip(): ), ) + def test_xfailed_step(allure_pytest_bdd_runner: AllurePytestRunner): feature_content = ( """ From 6ae4eb8893eb082f4705b2c4e6e4600cc33e1a2f Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:46:24 +0700 Subject: [PATCH 20/26] refactor(pytest-bdd): replace equal_to with strings --- .../acceptance/title_test.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/tests/allure_pytest_bdd/acceptance/title_test.py b/tests/allure_pytest_bdd/acceptance/title_test.py index f9671845..84dd65fc 100644 --- a/tests/allure_pytest_bdd/acceptance/title_test.py +++ b/tests/allure_pytest_bdd/acceptance/title_test.py @@ -1,5 +1,4 @@ from hamcrest import assert_that -from hamcrest import equal_to from allure_commons_test.report import has_test_case from allure_commons_test.result import has_title @@ -40,10 +39,8 @@ def given_noop(): allure_results, has_test_case( "sample.feature:Bar", - has_title( - equal_to("Lorem Ipsum"), - ) - ) + has_title("Lorem Ipsum"), + ), ) @@ -86,10 +83,8 @@ def given_noop(): allure_results, has_test_case( "sample.feature:Bar", - has_title( - equal_to("Lorem Ipsum"), - ) - ) + has_title("Lorem Ipsum"), + ), ) @@ -127,8 +122,6 @@ def given_noop(): allure_results, has_test_case( "sample.feature:Bar", - has_title( - equal_to("Lorem Ipsum"), - ) - ) + has_title("Lorem Ipsum"), + ), ) From 69f215a6b753fde68dc038186045c18f07554982 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:49:20 +0700 Subject: [PATCH 21/26] feat(pytest-bdd): remove params from scenario names --- allure-pytest-bdd/src/utils.py | 4 -- .../acceptance/title_test.py | 38 +++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index f5133d5c..bfa9ab60 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -154,10 +154,6 @@ def get_test_name(node, scenario, params): if allure_name: return interpolate_args(allure_name, params) - if hasattr(node, 'callspec'): - parts = node.nodeid.rsplit("[") - params = parts[-1] - return f"{scenario.name} [{params}" return scenario.name diff --git a/tests/allure_pytest_bdd/acceptance/title_test.py b/tests/allure_pytest_bdd/acceptance/title_test.py index 84dd65fc..1ba9343e 100644 --- a/tests/allure_pytest_bdd/acceptance/title_test.py +++ b/tests/allure_pytest_bdd/acceptance/title_test.py @@ -125,3 +125,41 @@ def given_noop(): has_title("Lorem Ipsum"), ), ) + + +def test_default_title_or_parametrized_test(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + + @pytest.mark.parametrize("foo", ["bar"]) + @scenario("sample.feature", "Bar") + def test_scenario(foo): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_title("Bar"), + ), + ) From 7aac712705ed8293aa2ad37a61099e5fc0883137 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 21 Mar 2025 17:17:01 +0700 Subject: [PATCH 22/26] feat(pytest-bdd): allure.title for steps --- allure-pytest-bdd/src/allure_api_listener.py | 10 +- allure-pytest-bdd/src/plugin.py | 2 - allure-pytest-bdd/src/pytest_bdd_listener.py | 12 +- allure-pytest-bdd/src/steps.py | 38 +- allure-pytest-bdd/src/storage.py | 20 +- allure-pytest-bdd/src/utils.py | 31 +- .../acceptance/title_test.py | 367 ++++++++++++++++++ 7 files changed, 446 insertions(+), 34 deletions(-) diff --git a/allure-pytest-bdd/src/allure_api_listener.py b/allure-pytest-bdd/src/allure_api_listener.py index 1a1f532b..e132e8e2 100644 --- a/allure-pytest-bdd/src/allure_api_listener.py +++ b/allure-pytest-bdd/src/allure_api_listener.py @@ -11,7 +11,7 @@ from .utils import ALLURE_DESCRIPTION_MARK from .utils import ALLURE_LABEL_MARK from .utils import ALLURE_LINK_MARK -from .utils import ALLURE_TITLE_MARK +from .utils import ALLURE_TITLE_ATTR from .utils import apply_link_pattern from .utils import attach_data @@ -28,8 +28,12 @@ def __init__(self, config, lifecycle): @allure_commons.hookimpl def decorate_as_title(self, test_title): - allure_title_mark = getattr(pytest.mark, ALLURE_TITLE_MARK) - return allure_title_mark(test_title) + + def decorator(fn): + setattr(fn, ALLURE_TITLE_ATTR, test_title) + return fn + + return decorator @allure_commons.hookimpl def add_title(self, test_title): diff --git a/allure-pytest-bdd/src/plugin.py b/allure-pytest-bdd/src/plugin.py index ae331e11..521eadc4 100644 --- a/allure-pytest-bdd/src/plugin.py +++ b/allure-pytest-bdd/src/plugin.py @@ -8,7 +8,6 @@ from .allure_api_listener import AllurePytestBddApiHooks from .pytest_bdd_listener import PytestBDDListener -from .utils import ALLURE_TITLE_MARK from .utils import ALLURE_DESCRIPTION_MARK from .utils import ALLURE_DESCRIPTION_HTML_MARK from .utils import ALLURE_LABEL_MARK @@ -58,7 +57,6 @@ def clean_up(): def register_marks(config): - config.addinivalue_line("markers", f"{ALLURE_TITLE_MARK}: allure title marker") config.addinivalue_line("markers", f"{ALLURE_DESCRIPTION_MARK}: allure description") config.addinivalue_line("markers", f"{ALLURE_DESCRIPTION_HTML_MARK}: allure description in HTML") config.addinivalue_line("markers", f"{ALLURE_LABEL_MARK}: allure label marker") diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index 4d3fbe00..25a2b36c 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -13,6 +13,7 @@ from .steps import report_undefined_step from .steps import start_gherkin_step from .steps import stop_gherkin_step +from .steps import update_step_name from .storage import save_excinfo from .storage import save_test_data from .utils import attach_data @@ -43,14 +44,16 @@ def __init__(self, lifecycle): def pytest_bdd_before_scenario(self, request, feature, scenario): item = request.node uuid = get_uuid(item.nodeid) + outline_params = get_outline_params(item) pytest_params = get_pytest_params(item) params = {**pytest_params, **outline_params} + save_test_data( item=item, feature=feature, scenario=scenario, - pytest_params=pytest_params, + params=params, ) full_name = get_full_name(feature, scenario) @@ -80,7 +83,12 @@ def pytest_bdd_after_scenario(self, request, feature, scenario): @pytest.hookimpl def pytest_bdd_before_step(self, request, feature, scenario, step, step_func): - start_gherkin_step(self.lifecycle, request.node, step) + start_gherkin_step(self.lifecycle, request.node, step, step_func) + + @pytest.hookimpl + def pytest_bdd_before_step_call(self, request, feature, scenario, step, step_func, step_func_args): + step_uuid = get_step_uuid(step) + update_step_name(self.lifecycle, request.node, step_uuid, step_func, step_func_args) @pytest.hookimpl def pytest_bdd_after_step(self, request, feature, scenario, step, step_func, step_func_args): diff --git a/allure-pytest-bdd/src/steps.py b/allure-pytest-bdd/src/steps.py index cb0f5c49..c8b507db 100644 --- a/allure-pytest-bdd/src/steps.py +++ b/allure-pytest-bdd/src/steps.py @@ -4,15 +4,28 @@ from allure_commons.utils import format_exception from allure_commons.utils import represent +from .storage import get_saved_params +from .storage import get_test_data from .storage import save_reported_step +from .utils import get_allure_title from .utils import get_uuid from .utils import get_status from .utils import get_status_details -from .utils import get_test_data -def get_step_name(step): - return f"{step.keyword} {step.name}" +def get_step_name(item, step, step_func, step_func_args=None): + return get_allure_title_of_step(item, step_func, step_func_args) or \ + f"{step.keyword} {step.name}" + + +def get_allure_title_of_step(item, step_func, step_func_args): + return get_allure_title( + step_func, + { + **(get_saved_params(item) or {}), + **(step_func_args or {}), + }, + ) def get_step_uuid(step): @@ -41,18 +54,31 @@ def stop_step(lifecycle, uuid, status=None, status_details=None, exception=None, return True -def start_gherkin_step(lifecycle, item, step, step_uuid=None): +def start_gherkin_step(lifecycle, item, step, step_func=None, step_uuid=None): if step_uuid is None: step_uuid = get_step_uuid(step) start_step( lifecycle, step_uuid=step_uuid, - title=get_step_name(step), + title=get_step_name(item, step, step_func), parent_uuid=get_uuid(item.nodeid), ) +def update_step_name(lifecycle, item, step_uuid, step_func, step_func_args): + if not step_func_args: + return + + new_name = get_allure_title_of_step(item, step_func, step_func_args) + if new_name is None: + return + + with lifecycle.update_step(uuid=step_uuid) as step_result: + if step_result is not None: + step_result.name = new_name + + def stop_gherkin_step(lifecycle, item, step_uuid, **kwargs): res = stop_step(lifecycle, step_uuid, **kwargs) if res: @@ -68,7 +94,7 @@ def ensure_gherkin_step_reported(lifecycle, item, step, step_uuid=None, **kwargs if stop_gherkin_step(lifecycle, item, step_uuid, **kwargs): return - start_gherkin_step(lifecycle, item, step, step_uuid) + start_gherkin_step(lifecycle, item, step, step_uuid=step_uuid) stop_gherkin_step(lifecycle, item, step_uuid, **kwargs) diff --git a/allure-pytest-bdd/src/storage.py b/allure-pytest-bdd/src/storage.py index a2060c4b..deb031d6 100644 --- a/allure-pytest-bdd/src/storage.py +++ b/allure-pytest-bdd/src/storage.py @@ -6,22 +6,30 @@ class AllurePytestBddTestData: - def __init__(self, feature, scenario, pytest_params): + def __init__(self, feature, scenario, params): self.feature = feature self.scenario = scenario - self.pytest_params = pytest_params + self.params = params self.excinfo = None self.reported_steps = set() -def save_test_data(item, feature, scenario, pytest_params): +def save_test_data(item, feature, scenario, params): item.stash[ALLURE_PYTEST_BDD_HASHKEY] = AllurePytestBddTestData( feature=feature, scenario=scenario, - pytest_params=pytest_params, + params=params, ) +def get_test_data(item): + return item.stash.get(ALLURE_PYTEST_BDD_HASHKEY, (None, None)) + + +def get_saved_params(item): + return get_test_data(item).params + + def save_excinfo(item, excinfo): test_data = get_test_data(item) if test_data: @@ -32,7 +40,3 @@ def save_reported_step(item, step_uuid): test_data = get_test_data(item) if test_data: test_data.reported_steps.add(step_uuid) - - -def get_test_data(item): - return item.stash.get(ALLURE_PYTEST_BDD_HASHKEY, (None, None)) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index bfa9ab60..eabc1e30 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -21,9 +21,9 @@ from .storage import get_test_data +ALLURE_TITLE_ATTR = "__allure_display_name__" ALLURE_DESCRIPTION_MARK = "allure_description" ALLURE_DESCRIPTION_HTML_MARK = "allure_description_html" -ALLURE_TITLE_MARK = "allure_title" ALLURE_LABEL_MARK = 'allure_label' ALLURE_LINK_MARK = 'allure_link' @@ -37,8 +37,21 @@ } -def get_allure_title(item): - return get_marker_value(item, ALLURE_TITLE_MARK) +def get_allure_title_of_test(item, params): + obj = getattr(item, "obj", None) + if obj is not None: + return get_allure_title(obj, params) + + +def get_allure_title(fn, kwargs): + if fn is not None: + title_format = getattr(fn, ALLURE_TITLE_ATTR, None) + if title_format: + return interpolate(title_format, kwargs) + + +def interpolate(format_str, kwargs): + return SafeFormatter().format(format_str, **kwargs) if kwargs else format_str def get_allure_description(item, feature, scenario): @@ -120,10 +133,6 @@ def get_marker_value(item, keyword): return marker.args[0] if marker and marker.args else None -def interpolate_args(format_str, args): - return SafeFormatter().format(format_str, **args) if args else format_str - - def should_convert_mark_to_tag(mark): return mark.name not in MARK_NAMES_TO_IGNORE and\ not mark.args and not mark.kwargs @@ -150,11 +159,7 @@ def resolve_description(description): def get_test_name(node, scenario, params): - allure_name = get_allure_title(node) - if allure_name: - return interpolate_args(allure_name, params) - - return scenario.name + return get_allure_title_of_test(node, params) or scenario.name def get_full_name(feature, scenario): @@ -278,7 +283,7 @@ def post_process_test_result(item, test_result): test_result.historyId = get_history_id( test_case_id=test_result.testCaseId, parameters=test_result.parameters, - pytest_params=test_data.pytest_params, + pytest_params=test_data.params, ) diff --git a/tests/allure_pytest_bdd/acceptance/title_test.py b/tests/allure_pytest_bdd/acceptance/title_test.py index 1ba9343e..dfb1ce4d 100644 --- a/tests/allure_pytest_bdd/acceptance/title_test.py +++ b/tests/allure_pytest_bdd/acceptance/title_test.py @@ -1,7 +1,10 @@ from hamcrest import assert_that +from hamcrest import anything from allure_commons_test.report import has_test_case from allure_commons_test.result import has_title +from allure_commons_test.result import has_step +from allure_commons_test.result import with_steps from tests.allure_pytest.pytest_runner import AllurePytestRunner @@ -163,3 +166,367 @@ def given_noop(): has_title("Bar"), ), ) + + +def test_step_title_decorator(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @allure.title("Lorem Ipsum") + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step("Lorem Ipsum"), + ), + ) + + +def test_step_title_interpolation_step_args(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given the 'Lorem' string + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given, parsers + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @allure.title("{foo} Ipsum") + @given(parsers.parse("the '{foo}' string")) + def given_string(foo): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step("Lorem Ipsum"), + ), + ) + + +def test_step_title_interpolation_fixture(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given, then, parsers + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @pytest.fixture + def foo(): + yield "Lorem Ipsum" + + @allure.title("{foo}") + @given("noop") + def given_noop(foo): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step("Lorem Ipsum"), + ), + ) + + +def test_step_title_interpolation_target_fixtures(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given a target fixture + Then the value gets interpolated + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given, then, parsers + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("a target fixture", target_fixture="foo") + def given_fixture(): + return "Lorem" + + @allure.title("{foo} Ipsum") + @then("the value gets interpolated") + def then_value_interpolated(foo): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_steps( + anything(), + has_title("Lorem Ipsum"), + ), + ), + ) + + +def test_step_title_interpolation_pytest_params_explicit(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @pytest.mark.parametrize("foo", ["Lorem"]) + @scenario("sample.feature", "Bar") + def test_scenario(foo): + pass + + @allure.title("{foo} Ipsum") + @given("noop") + def given_noop(foo): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step("Lorem Ipsum"), + ), + ) + + +def test_step_title_interpolation_pytest_params_implicit(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @pytest.mark.parametrize("foo", ["Lorem"]) + @scenario("sample.feature", "Bar") + def test_scenario(foo): + pass + + @allure.title("{foo} Ipsum") + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step("Lorem Ipsum"), + ), + ) + + +def test_step_title_interpolation_outline_params(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario Outline: Bar + Given noop + + Examples: + | foo | bar | + | Lorem | Ipsum | + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @allure.title("{foo} {bar}") + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step("Lorem Ipsum"), + ), + ) + + +def test_step_title_interpolation_priority(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario Outline: Bar + Given target fixture + Then value 'Lorem Ipsum' received + Then target fixture received + Then outline param used + Then pytest param used + + Examples: + | foo | bar | + | Outline | Outline | + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given, then, parsers + import allure + + @pytest.mark.parametrize(["foo", "bar"], [("Mark", "Mark")]) + @scenario("sample.feature", "Bar") + def test_scenario(foo, bar): + pass + + @given("target fixture", target_fixture="foo") + def given_target_fixture(): + return "Target Fixture" + + @allure.title("{foo}") + @then(parsers.parse("value '{foo}' received")) + def then_value_received(foo): + pass + + @allure.title("{foo}") + @then("target fixture received") + def then_target_fixture_received(foo): + pass + + @allure.title("{foo}") + @then("outline param used") + def then_outline_param_used(): + pass + + @allure.title("{bar}") + @then("pytest param used") + def then_pytest_param_used(bar): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + with_steps( + anything(), + has_title("Lorem Ipsum"), + has_title("Target Fixture"), + has_title("Outline"), + has_title("Mark"), + ), + ), + ) From 64d83ea8ff06435ba69dc7924fa878d904fee6d8 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 21 Mar 2025 23:08:33 +0700 Subject: [PATCH 23/26] feat(pytest-bdd): step arguments support --- allure-pytest-bdd/src/pytest_bdd_listener.py | 5 +- allure-pytest-bdd/src/steps.py | 66 +++++++++++ allure-pytest-bdd/src/utils.py | 15 ++- allure-python-commons-test/src/result.py | 14 ++- requirements/testing.txt | 1 + .../acceptance/attachments_test.py | 110 +++++++++++++++++- .../acceptance/steps/gherkin_steps_test.py | 55 +++++++++ tests/e2e.py | 23 ++++ 8 files changed, 277 insertions(+), 12 deletions(-) diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index 25a2b36c..d0697380 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -9,11 +9,11 @@ from allure_commons.utils import md5 from .steps import get_step_uuid +from .steps import process_gherkin_step_args from .steps import report_remaining_steps from .steps import report_undefined_step from .steps import start_gherkin_step from .steps import stop_gherkin_step -from .steps import update_step_name from .storage import save_excinfo from .storage import save_test_data from .utils import attach_data @@ -87,8 +87,7 @@ def pytest_bdd_before_step(self, request, feature, scenario, step, step_func): @pytest.hookimpl def pytest_bdd_before_step_call(self, request, feature, scenario, step, step_func, step_func_args): - step_uuid = get_step_uuid(step) - update_step_name(self.lifecycle, request.node, step_uuid, step_func, step_func_args) + process_gherkin_step_args(self.lifecycle, request.node, step, step_func, step_func_args) @pytest.hookimpl def pytest_bdd_after_step(self, request, feature, scenario, step, step_func, step_func_args): diff --git a/allure-pytest-bdd/src/steps.py b/allure-pytest-bdd/src/steps.py index c8b507db..cfc2741b 100644 --- a/allure-pytest-bdd/src/steps.py +++ b/allure-pytest-bdd/src/steps.py @@ -1,3 +1,4 @@ +from allure import attachment_type from allure_commons.model2 import StatusDetails from allure_commons.model2 import Status from allure_commons.model2 import Parameter @@ -7,6 +8,8 @@ from .storage import get_saved_params from .storage import get_test_data from .storage import save_reported_step +from .utils import attach_data +from .utils import format_csv from .utils import get_allure_title from .utils import get_uuid from .utils import get_status @@ -66,6 +69,65 @@ def start_gherkin_step(lifecycle, item, step, step_func=None, step_uuid=None): ) +def process_gherkin_step_args(lifecycle, item, step, step_func, step_func_args): + allure_step_params = dict(step_func_args) + step_uuid = get_step_uuid(step) + + docstring = step_func_args.get("docstring") + if try_attach_docstring(lifecycle, step_uuid, docstring): + del allure_step_params["docstring"] + + datatable = step_func_args.get("datatable") + if try_attach_datatable(lifecycle, step_uuid, datatable): + del allure_step_params["datatable"] + + add_step_parameters(lifecycle, step_uuid, allure_step_params) + + update_step_name(lifecycle, item, step_uuid, step_func, step_func_args) + + +def try_attach_docstring(lifecycle, step_uuid, docstring): + if isinstance(docstring, str): + attach_data( + lifecycle=lifecycle, + body=docstring, + name="Doc string", + attachment_type=attachment_type.TEXT, + parent_uuid=step_uuid, + ) + return True + return False + + +def try_attach_datatable(lifecycle, step_uuid, datatable): + if is_datatable(datatable): + attach_data( + lifecycle=lifecycle, + body=format_csv(datatable), + name="Data table", + attachment_type=attachment_type.CSV, + parent_uuid=step_uuid, + ) + return True + return False + + +def add_step_parameters(lifecycle, step_uuid, step_params): + if not step_params: + return + + with lifecycle.update_step(uuid=step_uuid) as step_result: + if step_result is None: + return + + step_result.parameters.extend( + Parameter( + name=name, + value=represent(value), + ) for name, value in step_params.items() + ) + + def update_step_name(lifecycle, item, step_uuid, step_func, step_func_args): if not step_func_args: return @@ -79,6 +141,10 @@ def update_step_name(lifecycle, item, step_uuid, step_func, step_func_args): step_result.name = new_name +def is_datatable(value): + return isinstance(value, list) and all(isinstance(row, list) for row in value) + + def stop_gherkin_step(lifecycle, item, step_uuid, **kwargs): res = stop_step(lifecycle, step_uuid, **kwargs) if res: diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index eabc1e30..43cc74af 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -1,3 +1,5 @@ +import csv +import io import os from urllib.parse import urlparse from uuid import UUID @@ -287,17 +289,18 @@ def post_process_test_result(item, test_result): ) -def attach_data(lifecycle, body, name, attachment_type, extension): +def attach_data(lifecycle, body, name, attachment_type, extension=None, parent_uuid=None): lifecycle.attach_data( uuid4(), body, name=name, attachment_type=attachment_type, extension=extension, + parent_uuid=parent_uuid, ) -def attach_file(lifecycle, source, name, attachment_type, extension): +def attach_file(lifecycle, source, name, attachment_type, extension=None): lifecycle.attach_file( uuid4(), source, @@ -305,3 +308,11 @@ def attach_file(lifecycle, source, name, attachment_type, extension): attachment_type=attachment_type, extension=extension, ) + + +def format_csv(rows): + with io.StringIO() as buffer: + writer = csv.writer(buffer) + writer.writerow(rows[0]) + writer.writerows(rows[1:]) + return buffer.getvalue() diff --git a/allure-python-commons-test/src/result.py b/allure-python-commons-test/src/result.py index 9206e61d..84bb6094 100644 --- a/allure-python-commons-test/src/result.py +++ b/allure-python-commons-test/src/result.py @@ -122,12 +122,14 @@ def has_parameter(name, value, *matchers): def doesnt_have_parameter(name): - return has_entry('parameters', - not_( - has_item( - has_entry('name', equal_to(name)), - ) - )) + return not_( + has_entry( + "parameters", + has_item( + has_entry("name", name), + ), + ), + ) def resolve_link_attr_matcher(key, value): diff --git a/requirements/testing.txt b/requirements/testing.txt index 09919cdf..fcea2af5 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -1,6 +1,7 @@ -r ./core.txt docutils mock +packaging poethepoet PyHamcrest Pygments diff --git a/tests/allure_pytest_bdd/acceptance/attachments_test.py b/tests/allure_pytest_bdd/acceptance/attachments_test.py index 357dcd01..c6b74255 100644 --- a/tests/allure_pytest_bdd/acceptance/attachments_test.py +++ b/tests/allure_pytest_bdd/acceptance/attachments_test.py @@ -1,10 +1,16 @@ +import pytest + from hamcrest import assert_that from hamcrest import equal_to, ends_with +from allure_commons_test.content import csv_equivalent from allure_commons_test.report import has_test_case -from allure_commons_test.result import has_attachment_with_content, has_step +from allure_commons_test.result import has_attachment_with_content +from allure_commons_test.result import has_step +from allure_commons_test.result import doesnt_have_parameter from tests.allure_pytest.pytest_runner import AllurePytestRunner +from tests.e2e import version_unmet def test_attach_content_from_scenario_function(allure_pytest_bdd_runner: AllurePytestRunner): @@ -178,3 +184,105 @@ def when_file_is_attached(): ), ), ) + + +@pytest.mark.skipif(version_unmet("pytest-bdd", 8), reason="Data tables support added in 8.0.0") +def test_attach_datatable(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given a datatable: + | foo | bar | + | baz | qux | + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("a datatable:") + def given_datatable(datatable): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given a datatable:", + has_attachment_with_content( + allure_results.attachments, + csv_equivalent([ + ["foo", "bar"], + ["baz", "qux"], + ]), + name="Data table", + attach_type="text/csv", + ), + doesnt_have_parameter("datatable"), + ), + ), + ) + + +@pytest.mark.skipif(version_unmet("pytest-bdd", 8), reason="Doc strings support added in 8.0.0") +def test_attach_docstring(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + ''' + Feature: Foo + Scenario: Bar + Given a docstring: + """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit + """ + ''' + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + import allure + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("a docstring:") + def given_docstring(docstring): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given a docstring:", + has_attachment_with_content( + allure_results.attachments, + "Lorem ipsum dolor sit amet, consectetur adipiscing elit", + name="Doc string", + attach_type="text/plain", + ), + doesnt_have_parameter("docstring"), + ), + ), + ) diff --git a/tests/allure_pytest_bdd/acceptance/steps/gherkin_steps_test.py b/tests/allure_pytest_bdd/acceptance/steps/gherkin_steps_test.py index d7c7fc1a..268e20a3 100644 --- a/tests/allure_pytest_bdd/acceptance/steps/gherkin_steps_test.py +++ b/tests/allure_pytest_bdd/acceptance/steps/gherkin_steps_test.py @@ -5,6 +5,7 @@ from allure_commons_test.report import has_test_case from allure_commons_test.result import has_title from allure_commons_test.result import has_step +from allure_commons_test.result import has_parameter from allure_commons_test.result import with_steps from allure_commons_test.result import with_status from allure_commons_test.result import has_status_details @@ -472,3 +473,57 @@ def then_skip(): ), ), ) + + +def test_gherkin_step_args(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given a target fixture + Then parameters (including 'from step name') are added + """ + ) + steps_content = ( + """ + import pytest + from pytest_bdd import scenario, given, then, parsers + import allure + + @pytest.fixture + def foo(): + yield "from fixture" + + @pytest.mark.parametrize("bar", ["from parametrize mark"]) + @scenario("sample.feature", "Bar") + def test_scenario(bar): + pass + + @given("a target fixture", target_fixture="baz") + def given_fixture(): + return "from target fixture" + + @then(parsers.parse("parameters (including '{qux}') are added")) + def then_parameters_added(foo, bar, baz, qux): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Then parameters (including 'from step name') are added", + has_parameter("foo", "'from fixture'"), + has_parameter("bar", "'from parametrize mark'"), + has_parameter("baz", "'from target fixture'"), + has_parameter("qux", "'from step name'"), + ), + ), + ) diff --git a/tests/e2e.py b/tests/e2e.py index 0280361a..cea605ba 100644 --- a/tests/e2e.py +++ b/tests/e2e.py @@ -13,6 +13,9 @@ import warnings from abc import abstractmethod from contextlib import contextmanager, ExitStack +from functools import lru_cache +from importlib.metadata import version as get_version_metadata +from packaging.version import parse as parse_version from pathlib import Path from pytest import FixtureRequest, Pytester, MonkeyPatch from typing import Tuple, Mapping, TypeVar, Generator, Callable, Union @@ -22,6 +25,26 @@ from allure_commons_test.report import AllureReport +@lru_cache(maxsize=None) +def version(package: str): + return parse_version(get_version_metadata(package)) + + +@lru_cache(maxsize=None) +def version_unmet(package: str, major: int, minor: int = 0, micro: int = 0): + + """Returns `True` is the version of the package doesn't meet the specified requirements. + + You may call this function in a @pytest.mark.skipif condition. + """ + + package_version = version(package) + req = (major, minor, micro) + if package_version.release == req: + return package_version.is_prerelease + return package_version.release < req + + PathlikeT = Union[str, Path] From 09b6ad2bfb970d90cc1d11cdfea0164f325b1bd0 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Mon, 24 Mar 2025 23:19:14 +0700 Subject: [PATCH 24/26] test(pytest-bdd): cover module level markers --- .../acceptance/description_test.py | 78 +++++++++++++++++++ .../acceptance/labels/labels_test.py | 41 ++++++++++ .../acceptance/links/default_links_test.py | 37 +++++++++ 3 files changed, 156 insertions(+) diff --git a/tests/allure_pytest_bdd/acceptance/description_test.py b/tests/allure_pytest_bdd/acceptance/description_test.py index 049305c4..3b6f82e4 100644 --- a/tests/allure_pytest_bdd/acceptance/description_test.py +++ b/tests/allure_pytest_bdd/acceptance/description_test.py @@ -54,6 +54,45 @@ def given_noop(): ) +def test_description_at_module_level(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenarios, given + import allure + + pytestmark = [allure.description("Lorem Ipsum")] + + scenarios("sample.feature") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_description( + equal_to("Lorem Ipsum"), + ) + ) + ) + + def test_description_html_decorator(allure_pytest_bdd_runner: AllurePytestRunner): feature_content = ( """ @@ -94,6 +133,45 @@ def given_noop(): ) +def test_description_html_decorator_at_module_level(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenarios, given + import allure + + pytestmark = [allure.description_html("Lorem Ipsum")] + + scenarios("sample.feature") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_description_html( + equal_to("Lorem Ipsum"), + ) + ) + ) + + def test_dynamic_description(allure_pytest_bdd_runner: AllurePytestRunner): feature_content = ( """ diff --git a/tests/allure_pytest_bdd/acceptance/labels/labels_test.py b/tests/allure_pytest_bdd/acceptance/labels/labels_test.py index 40e13d02..e2890e76 100644 --- a/tests/allure_pytest_bdd/acceptance/labels/labels_test.py +++ b/tests/allure_pytest_bdd/acceptance/labels/labels_test.py @@ -103,6 +103,47 @@ def given_noop(): ) +def test_label_decorator_at_module_level(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenarios, given + import allure + + pytestmark = [allure.label("foo", "bar", "baz")] + + scenarios("sample.feature") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + all_of( + has_label("foo", equal_to("bar")), + has_label("foo", equal_to("baz")), + ), + + ) + ) + + def test_dynamic_label(allure_pytest_bdd_runner: AllurePytestRunner): feature_content = ( """ diff --git a/tests/allure_pytest_bdd/acceptance/links/default_links_test.py b/tests/allure_pytest_bdd/acceptance/links/default_links_test.py index 8e307584..7d10c707 100644 --- a/tests/allure_pytest_bdd/acceptance/links/default_links_test.py +++ b/tests/allure_pytest_bdd/acceptance/links/default_links_test.py @@ -44,6 +44,43 @@ def given_noop(): ) +def test_link_decorator_at_module_level(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenarios, given + import allure + + pytestmark = [allure.link("https://allurereport.org")] + + scenarios("sample.feature") + + @given("noop") + def given_noop(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_link("https://allurereport.org", link_type="link"), + ), + ) + + def test_named_link_decorator(allure_pytest_bdd_runner: AllurePytestRunner): feature_content = ( """ From 69da6e927f64a23ff3a22c3c3a4bf522b16bc600 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Mon, 24 Mar 2025 23:19:28 +0700 Subject: [PATCH 25/26] test(pytest-bdd): cover attach from hooks --- .../acceptance/attachments_test.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/allure_pytest_bdd/acceptance/attachments_test.py b/tests/allure_pytest_bdd/acceptance/attachments_test.py index c6b74255..1b3ebb8a 100644 --- a/tests/allure_pytest_bdd/acceptance/attachments_test.py +++ b/tests/allure_pytest_bdd/acceptance/attachments_test.py @@ -186,6 +186,54 @@ def when_file_is_attached(): ) +def test_attach_file_from_hook(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given noop + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given("noop") + def given_noop(): + pass + """ + ) + conftest_content = ( + """ + import allure + def pytest_runtest_teardown(item): + allure.attach.file(__file__, name="foo") + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + conftest_literal=conftest_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_attachment_with_content( + allure_results.attachments, + ends_with("conftest.py"), + name="foo", + ), + ), + ) + + @pytest.mark.skipif(version_unmet("pytest-bdd", 8), reason="Data tables support added in 8.0.0") def test_attach_datatable(allure_pytest_bdd_runner: AllurePytestRunner): feature_content = ( From 1174859ff4330df7539e1a267c582ac0e719b694 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Mon, 24 Mar 2025 23:59:37 +0700 Subject: [PATCH 26/26] test(pytest-bdd): cover docstr and datatables backward compat --- .../acceptance/attachments_test.py | 209 +++++++++++++++++- tests/e2e.py | 13 +- 2 files changed, 215 insertions(+), 7 deletions(-) diff --git a/tests/allure_pytest_bdd/acceptance/attachments_test.py b/tests/allure_pytest_bdd/acceptance/attachments_test.py index 1b3ebb8a..907e6604 100644 --- a/tests/allure_pytest_bdd/acceptance/attachments_test.py +++ b/tests/allure_pytest_bdd/acceptance/attachments_test.py @@ -1,16 +1,21 @@ import pytest from hamcrest import assert_that -from hamcrest import equal_to, ends_with +from hamcrest import equal_to +from hamcrest import ends_with +from hamcrest import not_ from allure_commons_test.content import csv_equivalent from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_attachment from allure_commons_test.result import has_attachment_with_content from allure_commons_test.result import has_step +from allure_commons_test.result import has_parameter from allure_commons_test.result import doesnt_have_parameter from tests.allure_pytest.pytest_runner import AllurePytestRunner -from tests.e2e import version_unmet +from tests.e2e import version_lt +from tests.e2e import version_gte def test_attach_content_from_scenario_function(allure_pytest_bdd_runner: AllurePytestRunner): @@ -234,7 +239,7 @@ def pytest_runtest_teardown(item): ) -@pytest.mark.skipif(version_unmet("pytest-bdd", 8), reason="Data tables support added in 8.0.0") +@pytest.mark.skipif(version_lt("pytest-bdd", 8), reason="Data tables support added in 8.0.0") def test_attach_datatable(allure_pytest_bdd_runner: AllurePytestRunner): feature_content = ( """ @@ -248,7 +253,6 @@ def test_attach_datatable(allure_pytest_bdd_runner: AllurePytestRunner): steps_content = ( """ from pytest_bdd import scenario, given - import allure @scenario("sample.feature", "Bar") def test_scenario(): @@ -286,7 +290,107 @@ def given_datatable(datatable): ) -@pytest.mark.skipif(version_unmet("pytest-bdd", 8), reason="Doc strings support added in 8.0.0") +@pytest.mark.skipif(version_gte("pytest-bdd", 8), reason="Pytest-BDD features proper data tables starting from 8.0") +def test_attach_datatable_compat_well_defined(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given a datatable: + | foo | bar | + | baz | qux | + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given, parsers + + def parse_data_table(text): + return [ + [x.strip() for x in line.split("|")] + for line in (x.strip("|") for x in text.splitlines()) + ] + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given(parsers.parse("a datatable:\\n{datatable:Datatable}", extra_types={"Datatable": parse_data_table})) + def given_datatable(datatable): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given a datatable:\n| foo | bar |\n| baz | qux |", + has_attachment_with_content( + allure_results.attachments, + csv_equivalent([ + ["foo", "bar"], + ["baz", "qux"], + ]), + name="Data table", + attach_type="text/csv", + ), + doesnt_have_parameter("datatable"), + ), + ), + ) + + +@pytest.mark.skipif(version_gte("pytest-bdd", 8), reason="Pytest-BDD features proper data tables starting from 8.0") +def test_attach_datatable_compat_string(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: Foo + Scenario: Bar + Given a datatable: + | foo | bar | + | baz | qux | + """ + ) + steps_content = ( + """ + from pytest_bdd import scenario, given, parsers + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given(parsers.parse("a datatable:\\n{datatable}")) + def given_datatable(datatable): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + "Given a datatable:\n| foo | bar |\n| baz | qux |", + not_(has_attachment(name="Data table")), + has_parameter("datatable", "'| foo | bar |\n| baz | qux |'"), + ), + ), + ) + + +@pytest.mark.skipif(version_lt("pytest-bdd", 8), reason="Doc strings support added in 8.0.0") def test_attach_docstring(allure_pytest_bdd_runner: AllurePytestRunner): feature_content = ( ''' @@ -301,7 +405,6 @@ def test_attach_docstring(allure_pytest_bdd_runner: AllurePytestRunner): steps_content = ( """ from pytest_bdd import scenario, given - import allure @scenario("sample.feature", "Bar") def test_scenario(): @@ -334,3 +437,97 @@ def given_docstring(docstring): ), ), ) + + +@pytest.mark.skipif(version_gte("pytest-bdd", 8), reason="Pytest-BDD features proper doc strings starting from 8.0") +def test_attach_docstring_compat_well_defined(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + ''' + Feature: Foo + Scenario: Bar + Given a docstring: + """Lorem Ipsum""" + ''' + ) + steps_content = ( + """ + from pytest_bdd import scenario, given, parsers + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given(parsers.parse('a docstring:\\n\"\"\"{docstring}\"\"\"')) + def given_docstring(docstring): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + 'Given a docstring:\n"""Lorem Ipsum"""', + has_attachment_with_content( + allure_results.attachments, + "Lorem Ipsum", + name="Doc string", + attach_type="text/plain", + ), + doesnt_have_parameter("docstring"), + ), + ), + ) + + +@pytest.mark.skipif(version_gte("pytest-bdd", 8), reason="Pytest-BDD features proper doc strings starting from 8.0") +def test_attach_datatable_compat_not_string(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + ''' + Feature: Foo + Scenario: Bar + Given a docstring: + """Lorem Ipsum""" + ''' + ) + steps_content = ( + """ + from pytest_bdd import scenario, given, parsers + + @scenario("sample.feature", "Bar") + def test_scenario(): + pass + + @given( + parsers.parse( + 'a docstring:\\n\"\"\"{docstring:Converted}\"\"\"', + extra_types={"Converted": lambda _: 0}, + ), + ) + def given_docstring(docstring): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + ) + + assert_that( + allure_results, + has_test_case( + "sample.feature:Bar", + has_step( + 'Given a docstring:\n"""Lorem Ipsum"""', + not_(has_attachment(name="Doc string")), + has_parameter("docstring", "0"), + ), + ), + ) diff --git a/tests/e2e.py b/tests/e2e.py index cea605ba..5453e6fa 100644 --- a/tests/e2e.py +++ b/tests/e2e.py @@ -31,7 +31,7 @@ def version(package: str): @lru_cache(maxsize=None) -def version_unmet(package: str, major: int, minor: int = 0, micro: int = 0): +def version_lt(package: str, major: int, minor: int = 0, micro: int = 0): """Returns `True` is the version of the package doesn't meet the specified requirements. @@ -45,6 +45,17 @@ def version_unmet(package: str, major: int, minor: int = 0, micro: int = 0): return package_version.release < req +@lru_cache(maxsize=None) +def version_gte(package: str, major: int, minor: int = 0, micro: int = 0): + + """Returns `True` is the version of the package meets the specified requirements. + + You may call this function in a @pytest.mark.skipif condition. + """ + + return not version_lt(package, major, minor, micro) + + PathlikeT = Union[str, Path]