diff --git a/allure-behave/src/listener.py b/allure-behave/src/listener.py index c584f6b5..11f03658 100644 --- a/allure-behave/src/listener.py +++ b/allure-behave/src/listener.py @@ -5,6 +5,7 @@ from allure_commons.utils import md5 from allure_commons.utils import now from allure_commons.utils import platform_label +from allure_commons.utils import represent from allure_commons.types import LabelType, AttachmentType, LinkType from allure_commons.model2 import TestResult from allure_commons.model2 import TestStepResult @@ -163,6 +164,27 @@ def start_step(self, uuid, title, params): step = TestStepResult(name=title, start=now(), parameters=parameters) self.logger.start_step(None, uuid, step) + @allure_commons.hookimpl + def add_step_parameter(self, uuid, name, value, excluded, mode): + step = self.logger.get_item(uuid) + if step: + parameter = Parameter( + name=name, + value=represent(value), + excluded=excluded, + mode=mode.value if mode else None + ) + step.parameters.append(parameter) + + @allure_commons.hookimpl + def get_current_step_uuid(self): + items_list = list(self.logger._items) + for uuid in reversed(items_list): + item = self.logger.get_item(uuid) + if isinstance(item, TestStepResult): + return uuid + return None + @allure_commons.hookimpl def stop_step(self, uuid, exc_type, exc_val, exc_tb): self.logger.stop_step(uuid, diff --git a/allure-pytest/src/listener.py b/allure-pytest/src/listener.py index 21c06750..73246d5f 100644 --- a/allure-pytest/src/listener.py +++ b/allure-pytest/src/listener.py @@ -50,6 +50,27 @@ def start_step(self, uuid, title, params): step = TestStepResult(name=title, start=now(), parameters=parameters) self.allure_logger.start_step(None, uuid, step) + @allure_commons.hookimpl + def add_step_parameter(self, uuid, name, value, excluded, mode): + step = self.allure_logger.get_item(uuid) + if step: + parameter = Parameter( + name=name, + value=represent(value), + excluded=excluded, + mode=mode.value if mode else None + ) + step.parameters.append(parameter) + + @allure_commons.hookimpl + def get_current_step_uuid(self): + items_list = list(self.allure_logger._items) + for uuid in reversed(items_list): + item = self.allure_logger.get_item(uuid) + if isinstance(item, TestStepResult): + return uuid + return None + @allure_commons.hookimpl def stop_step(self, uuid, exc_type, exc_val, exc_tb): self.allure_logger.stop_step(uuid, diff --git a/allure-python-commons/src/allure/__init__.py b/allure-python-commons/src/allure/__init__.py index 4e72e402..4d0c0cb2 100644 --- a/allure-python-commons/src/allure/__init__.py +++ b/allure-python-commons/src/allure/__init__.py @@ -9,6 +9,7 @@ from allure_commons._allure import link, issue, testcase from allure_commons._allure import Dynamic as dynamic from allure_commons._allure import step +from allure_commons._allure import add_parameter from allure_commons._allure import attach from allure_commons._allure import manual from allure_commons.types import Severity as severity_level @@ -35,6 +36,7 @@ "testcase", "manual", "step", + "add_parameter", "dynamic", "severity_level", "attach", diff --git a/allure-python-commons/src/allure_commons/_allure.py b/allure-python-commons/src/allure_commons/_allure.py index 08fb5a87..2743e15e 100644 --- a/allure-python-commons/src/allure_commons/_allure.py +++ b/allure-python-commons/src/allure_commons/_allure.py @@ -171,6 +171,25 @@ def step(title: _TFunc) -> _TFunc: ... +def add_parameter(name, value, excluded=None, mode=None): + results = plugin_manager.hook.get_current_step_uuid() + current_step_uuid = results[0] if results else None + + if current_step_uuid is None: + raise RuntimeError( + "No active step found. Use 'allure.add_parameter()' only inside " + "a step context (with allure.step(...):) or a decorated step function." + ) + + plugin_manager.hook.add_step_parameter( + uuid=current_step_uuid, + name=name, + value=value, + excluded=excluded, + mode=mode + ) + + def step(title): if callable(title): return StepContext(title.__name__, {})(title) diff --git a/allure-python-commons/src/allure_commons/_hooks.py b/allure-python-commons/src/allure_commons/_hooks.py index 0ff19a27..acbb2020 100644 --- a/allure-python-commons/src/allure_commons/_hooks.py +++ b/allure-python-commons/src/allure_commons/_hooks.py @@ -50,6 +50,14 @@ def add_link(self, url, link_type, name): def add_parameter(self, name, value, excluded, mode): """ parameter """ + @hookspec + def add_step_parameter(self, uuid, name, value, excluded, mode): + """ step parameter """ + + @hookspec + def get_current_step_uuid(self): + """ get current active step uuid """ + @hookspec def start_step(self, uuid, title, params): """ step """ diff --git a/allure-robotframework/src/listener/allure_listener.py b/allure-robotframework/src/listener/allure_listener.py index 045c491a..4b300b99 100644 --- a/allure-robotframework/src/listener/allure_listener.py +++ b/allure-robotframework/src/listener/allure_listener.py @@ -10,8 +10,10 @@ from allure_commons.utils import platform_label from allure_commons.utils import host_tag from allure_commons.utils import format_exception, format_traceback +from allure_commons.utils import represent from allure_commons.model2 import Label, Link from allure_commons.model2 import Status, StatusDetails +from allure_commons.model2 import TestStepResult from allure_commons.model2 import Parameter from allure_commons.types import LabelType, AttachmentType, Severity, LinkType from allure_robotframework.utils import get_allure_status @@ -246,6 +248,27 @@ def start_step(self, uuid, title, params): step.start = now() step.parameters = [Parameter(name=name, value=value) for name, value in params.items()] + @allure_commons.hookimpl + def add_step_parameter(self, uuid, name, value, excluded, mode): + with self.lifecycle.update_step(uuid=uuid) as step: + if step: + parameter = Parameter( + name=name, + value=represent(value), + excluded=excluded, + mode=mode.value if mode else None + ) + step.parameters.append(parameter) + + @allure_commons.hookimpl + def get_current_step_uuid(self): + items_list = list(self.lifecycle._items) + for uuid in reversed(items_list): + item = self.lifecycle._items.get(uuid) + if isinstance(item, TestStepResult): + return uuid + return None + @allure_commons.hookimpl def stop_step(self, uuid, exc_type, exc_val, exc_tb): with self.lifecycle.update_step() as step: diff --git a/tests/allure_pytest/acceptance/step/add_parameter_test.py b/tests/allure_pytest/acceptance/step/add_parameter_test.py new file mode 100644 index 00000000..1f6010fe --- /dev/null +++ b/tests/allure_pytest/acceptance/step/add_parameter_test.py @@ -0,0 +1,236 @@ +from hamcrest import assert_that +from tests.allure_pytest.pytest_runner import AllurePytestRunner + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_step +from allure_commons_test.result import has_parameter +from allure_commons_test.result import get_parameter_matcher +from allure_commons_test.result import with_mode +from allure_commons_test.result import with_excluded +from allure_commons_test.result import with_status + + +def test_add_parameter_context_manager(allure_pytest_runner: AllurePytestRunner): + """ + >>> import allure + + >>> def test_add_parameter_context_manager(): + ... with allure.step("Step 1"): + ... allure.add_parameter("env", "production") + ... allure.add_parameter("version", "1.0.0") + """ + allure_results = allure_pytest_runner.run_docstring() + + assert_that( + allure_results, + has_test_case( + "test_add_parameter_context_manager", + has_step( + "Step 1", + has_parameter("env", "'production'"), + has_parameter("version", "'1.0.0'") + ) + ) + ) + + +def test_add_parameter_decorator(allure_pytest_runner: AllurePytestRunner): + """ + >>> import allure + + >>> @allure.step("Step 2") + ... def decorated_step(): + ... allure.add_parameter("decorated_param", "decorated_value") + + >>> def test_add_parameter_decorator(): + ... decorated_step() + """ + allure_results = allure_pytest_runner.run_docstring() + + assert_that( + allure_results, + has_test_case( + "test_add_parameter_decorator", + has_step( + "Step 2", + has_parameter("decorated_param", "'decorated_value'") + ) + ) + ) + + +def test_add_parameter_nested(allure_pytest_runner: AllurePytestRunner): + """ + >>> import allure + + >>> def test_add_parameter_nested(): + ... with allure.step("Parent Step"): + ... with allure.step("Child Step"): + ... allure.add_parameter("nested_param", "nested_value") + """ + allure_results = allure_pytest_runner.run_docstring() + + assert_that( + allure_results, + has_test_case( + "test_add_parameter_nested", + has_step( + "Parent Step", + has_step( + "Child Step", + has_parameter("nested_param", "'nested_value'") + ) + ) + ) + ) + + +def test_add_parameter_no_active_step_raises_error(allure_pytest_runner: AllurePytestRunner): + """ + >>> import allure + + >>> def test_add_parameter_no_active_step_raises_error(): + ... try: + ... allure.add_parameter("test", "value") + ... assert False, "Should raise RuntimeError" + ... except RuntimeError: + ... pass # Expected error + """ + allure_results = allure_pytest_runner.run_docstring() + + assert_that( + allure_results, + has_test_case( + "test_add_parameter_no_active_step_raises_error", + with_status("passed") + ) + ) + + +def test_add_parameter_with_mode(allure_pytest_runner: AllurePytestRunner): + """ + >>> import allure + + >>> def test_add_parameter_with_mode(): + ... with allure.step("Step 1"): + ... allure.add_parameter( + ... "password", "secret", mode=allure.parameter_mode.MASKED + ... ) + """ + allure_results = allure_pytest_runner.run_docstring() + + assert_that( + allure_results, + has_test_case( + "test_add_parameter_with_mode", + has_step( + "Step 1", + get_parameter_matcher( + "password", + with_mode("masked") + ) + ) + ) + ) + + +def test_add_parameter_hidden_mode(allure_pytest_runner: AllurePytestRunner): + """ + >>> import allure + + >>> def test_add_parameter_hidden_mode(): + ... with allure.step("Step 1"): + ... allure.add_parameter( + ... "environment", "staging", mode=allure.parameter_mode.HIDDEN + ... ) + """ + allure_results = allure_pytest_runner.run_docstring() + + assert_that( + allure_results, + has_test_case( + "test_add_parameter_hidden_mode", + has_step( + "Step 1", + get_parameter_matcher( + "environment", + with_mode("hidden") + ) + ) + ) + ) + + +def test_add_parameter_excluded(allure_pytest_runner: AllurePytestRunner): + """ + >>> import allure + + >>> def test_add_parameter_excluded(): + ... with allure.step("Step 1"): + ... allure.add_parameter("work-dir", "/tmp", excluded=True) + """ + allure_results = allure_pytest_runner.run_docstring() + + assert_that( + allure_results, + has_test_case( + "test_add_parameter_excluded", + has_step( + "Step 1", + get_parameter_matcher( + "work-dir", + with_excluded() + ) + ) + ) + ) + + +def test_add_parameter_multiple(allure_pytest_runner: AllurePytestRunner): + """ + >>> import allure + + >>> def test_add_parameter_multiple(): + ... with allure.step("Step 1"): + ... allure.add_parameter("env", "staging") + ... allure.add_parameter("version", "1.0.0") + ... allure.add_parameter("host", "localhost") + """ + allure_results = allure_pytest_runner.run_docstring() + + assert_that( + allure_results, + has_test_case( + "test_add_parameter_multiple", + has_step( + "Step 1", + has_parameter("env", "'staging'"), + has_parameter("version", "'1.0.0'"), + has_parameter("host", "'localhost'") + ) + ) + ) + + +def test_add_parameter_vs_test_parameter(allure_pytest_runner: AllurePytestRunner): + """ + >>> import allure + + >>> def test_add_parameter_vs_test_parameter(): + ... allure.dynamic.parameter("test_param", "test_value") + ... with allure.step("Step 1"): + ... allure.add_parameter("step_param", "step_value") + """ + allure_results = allure_pytest_runner.run_docstring() + + assert_that( + allure_results, + has_test_case( + "test_add_parameter_vs_test_parameter", + has_parameter("test_param", "'test_value'"), + has_step( + "Step 1", + has_parameter("step_param", "'step_value'") + ) + ) + )