diff --git a/allure-behave/src/listener.py b/allure-behave/src/listener.py index ad543526..a39d0e91 100644 --- a/allure-behave/src/listener.py +++ b/allure-behave/src/listener.py @@ -187,8 +187,8 @@ def global_attach_file(self, source, name, attachment_type, extension): self.logger.global_attach_file(uuid4(), source, name=name, attachment_type=attachment_type, extension=extension) @allure_commons.hookimpl - def global_error(self, message, trace): - self.logger.global_error(message=message, trace=trace) + def global_error(self, message, trace, status): + self.logger.global_error(message=message, trace=trace, status=status) @allure_commons.hookimpl def add_description(self, test_description): diff --git a/allure-pytest-bdd/src/allure_api_listener.py b/allure-pytest-bdd/src/allure_api_listener.py index 0b980725..0043a5c2 100644 --- a/allure-pytest-bdd/src/allure_api_listener.py +++ b/allure-pytest-bdd/src/allure_api_listener.py @@ -130,5 +130,5 @@ def global_attach_file(self, source, name, attachment_type, extension): global_attach_file(self.lifecycle, source, name, attachment_type, extension) @allure_commons.hookimpl - def global_error(self, message, trace): - global_error(self.lifecycle, message, trace) + def global_error(self, message, trace, status): + global_error(self.lifecycle, message, trace, status) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index cc35d9e0..02ed0b86 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -6,6 +6,7 @@ import pytest +from allure_commons.types import ErrorStatus from allure_commons.model2 import Label from allure_commons.model2 import Link from allure_commons.model2 import StatusDetails @@ -338,8 +339,8 @@ def global_attach_file(lifecycle, source, name, attachment_type, extension=None) ) -def global_error(lifecycle, message, trace=None): - lifecycle.global_error(message=message, trace=trace) +def global_error(lifecycle, message, trace=None, status=ErrorStatus.BROKEN): + lifecycle.global_error(message=message, trace=trace, status=status) def format_csv(rows): diff --git a/allure-pytest/src/listener.py b/allure-pytest/src/listener.py index 10ec29df..0685b61a 100644 --- a/allure-pytest/src/listener.py +++ b/allure-pytest/src/listener.py @@ -278,8 +278,8 @@ def global_attach_file(self, source, name, attachment_type, extension): ) @allure_commons.hookimpl - def global_error(self, message, trace): - self.allure_logger.global_error(message=message, trace=trace) + def global_error(self, message, trace, status): + self.allure_logger.global_error(message=message, trace=trace, status=status) @allure_commons.hookimpl def add_title(self, test_title): diff --git a/allure-python-commons/src/allure/__init__.py b/allure-python-commons/src/allure/__init__.py index 6fe2f270..86feb62f 100644 --- a/allure-python-commons/src/allure/__init__.py +++ b/allure-python-commons/src/allure/__init__.py @@ -16,6 +16,7 @@ from allure_commons.types import Severity as severity_level from allure_commons.types import AttachmentType as attachment_type from allure_commons.types import ParameterMode as parameter_mode +from allure_commons.types import ErrorStatus as error_status __all__ = [ @@ -43,5 +44,6 @@ "global_attach", "global_error", "attachment_type", - "parameter_mode" + "parameter_mode", + "error_status" ] diff --git a/allure-python-commons/src/allure_commons/_allure.py b/allure-python-commons/src/allure_commons/_allure.py index 607e1cb8..731786dc 100644 --- a/allure-python-commons/src/allure_commons/_allure.py +++ b/allure-python-commons/src/allure_commons/_allure.py @@ -2,7 +2,7 @@ from typing import Any, Callable, TypeVar, Union, overload from allure_commons._core import plugin_manager -from allure_commons.types import LabelType, LinkType, ParameterMode +from allure_commons.types import LabelType, LinkType, ParameterMode, ErrorStatus from allure_commons.utils import uuid4 from allure_commons.utils import format_exception, format_traceback from allure_commons.utils import func_parameters, represent @@ -238,25 +238,33 @@ def file(self, source, name=None, attachment_type=None, extension=None): global_attach = GlobalAttach() - @overload -def global_error(value: BaseException) -> None: +def global_error( + value: BaseException, + *, + status: ErrorStatus = ErrorStatus.BROKEN +) -> None: ... @overload -def global_error(value: str, trace: Union[str, None] = None) -> None: +def global_error( + value: str, + trace: Union[str, None] = None, + *, + status: ErrorStatus = ErrorStatus.BROKEN +) -> None: ... -def global_error(value, trace=None): +def global_error(value, trace=None, *, status: ErrorStatus = ErrorStatus.BROKEN): message = None if isinstance(value, BaseException): message = format_exception(type(value), value) trace = format_traceback(value.__traceback__) else: message = value - plugin_manager.hook.global_error(message=message, trace=trace) + plugin_manager.hook.global_error(message=message, trace=trace, status=status) class fixture: diff --git a/allure-python-commons/src/allure_commons/_hooks.py b/allure-python-commons/src/allure_commons/_hooks.py index 84e916d9..0ef73b8e 100644 --- a/allure-python-commons/src/allure_commons/_hooks.py +++ b/allure-python-commons/src/allure_commons/_hooks.py @@ -75,7 +75,7 @@ def global_attach_file(self, source, name, attachment_type, extension): """ attach global file """ @hookspec - def global_error(self, message, trace): + def global_error(self, message, trace, status): """ global error """ diff --git a/allure-python-commons/src/allure_commons/lifecycle.py b/allure-python-commons/src/allure_commons/lifecycle.py index e2c2251e..b97d6b82 100644 --- a/allure-python-commons/src/allure_commons/lifecycle.py +++ b/allure-python-commons/src/allure_commons/lifecycle.py @@ -9,6 +9,7 @@ from allure_commons.model2 import ExecutableItem from allure_commons.model2 import TestBeforeResult from allure_commons.model2 import TestAfterResult +from allure_commons.types import ErrorStatus from allure_commons.utils import uuid4 from allure_commons.utils import now from allure_commons.types import AttachmentType @@ -168,9 +169,9 @@ def global_attach_data(self, uuid, body, name=None, attachment_type=None, extens GlobalAttachment(source=file_name, name=name, type=mime_type, timestamp=now()) ])) - def global_error(self, message=None, trace=None): + def global_error(self, message=None, trace=None, status=ErrorStatus.BROKEN): plugin_manager.hook.report_globals(globals_item=Globals(errors=[ - GlobalError(message=message, trace=trace, timestamp=now()) + GlobalError(status=status, message=message, trace=trace, timestamp=now()) ])) def __resolve_attachment_filename_and_type(self, uuid, attachment_type=None, extension=None): diff --git a/allure-python-commons/src/allure_commons/model2.py b/allure-python-commons/src/allure_commons/model2.py index cd069b17..d2a307cc 100644 --- a/allure-python-commons/src/allure_commons/model2.py +++ b/allure-python-commons/src/allure_commons/model2.py @@ -1,5 +1,6 @@ from attr import attrs, attrib from attr import Factory +from allure_commons.types import ErrorStatus TEST_GROUP_PATTERN = "{prefix}-container.json" @@ -111,6 +112,7 @@ class GlobalAttachment(Attachment): @attrs class GlobalError(StatusDetails): + status = attrib(default=ErrorStatus.BROKEN) timestamp = attrib(default=None) diff --git a/allure-python-commons/src/allure_commons/reporter.py b/allure-python-commons/src/allure_commons/reporter.py index 7e7b7594..4cb011c2 100644 --- a/allure-python-commons/src/allure_commons/reporter.py +++ b/allure-python-commons/src/allure_commons/reporter.py @@ -6,6 +6,7 @@ from allure_commons.model2 import TestResult from allure_commons.model2 import Attachment, ATTACHMENT_PATTERN from allure_commons.model2 import GlobalAttachment, GlobalError, Globals +from allure_commons.types import ErrorStatus from allure_commons.utils import now from allure_commons._core import plugin_manager @@ -172,9 +173,9 @@ def global_attach_data(self, uuid, body, name=None, attachment_type=None, extens GlobalAttachment(source=file_name, name=name, type=mime_type, timestamp=now()) ])) - def global_error(self, message=None, trace=None): + def global_error(self, message=None, trace=None, status=ErrorStatus.BROKEN): plugin_manager.hook.report_globals(globals_item=Globals(errors=[ - GlobalError(message=message, trace=trace, timestamp=now()) + GlobalError(status=status, message=message, trace=trace, timestamp=now()) ])) def __resolve_attachment_filename_and_type(self, uuid, attachment_type=None, extension=None): diff --git a/allure-python-commons/src/allure_commons/types.py b/allure-python-commons/src/allure_commons/types.py index 9efa6c2a..89d70c99 100644 --- a/allure-python-commons/src/allure_commons/types.py +++ b/allure-python-commons/src/allure_commons/types.py @@ -71,3 +71,8 @@ class ParameterMode(Enum): HIDDEN = "hidden" MASKED = "masked" DEFAULT = None + + +class ErrorStatus(str, Enum): + BROKEN = "broken" + FAILED = "failed" diff --git a/allure-robotframework/src/library/allure_library.py b/allure-robotframework/src/library/allure_library.py index e9d4f7b1..bb731341 100644 --- a/allure-robotframework/src/library/allure_library.py +++ b/allure-robotframework/src/library/allure_library.py @@ -27,5 +27,5 @@ def global_attach_file(source, name=None, attachment_type=None, extension=None): allure.global_attach.file(source, name=name, attachment_type=_attachment_type(attachment_type), extension=extension) -def global_error(message, trace=None): - allure.global_error(message, trace=trace) +def global_error(message, trace=None, *, status = allure.error_status.BROKEN): + allure.global_error(message, trace=trace, status=status) diff --git a/allure-robotframework/src/listener/allure_listener.py b/allure-robotframework/src/listener/allure_listener.py index 6be71a19..eb55e5f7 100644 --- a/allure-robotframework/src/listener/allure_listener.py +++ b/allure-robotframework/src/listener/allure_listener.py @@ -260,8 +260,8 @@ def global_attach_file(self, source, name, attachment_type, extension): ) @allure_commons.hookimpl - def global_error(self, message, trace): - self.lifecycle.global_error(message=message, trace=trace) + def global_error(self, message, trace, status): + self.lifecycle.global_error(message=message, trace=trace, status=status) @allure_commons.hookimpl def start_step(self, uuid, title, params): diff --git a/tests/allure_behave/acceptance/allure_api/globals/globals_test.py b/tests/allure_behave/acceptance/allure_api/globals/globals_test.py index ce1a5f48..22bfb2d4 100644 --- a/tests/allure_behave/acceptance/allure_api/globals/globals_test.py +++ b/tests/allure_behave/acceptance/allure_api/globals/globals_test.py @@ -3,7 +3,7 @@ from hamcrest import assert_that, equal_to, has_item, has_length from allure_commons_test.result import has_global_attachment_with_content from allure_commons_test.result import has_global_error -from allure_commons_test.result import with_message_contains +from allure_commons_test.result import with_message_contains, with_status def test_global_attachment_and_error_hooks(behave_runner: Runner): @@ -50,7 +50,40 @@ def after_all(context): behave_runner.allure_results.globals, has_item( has_global_error( - with_message_contains("behave global error") + with_message_contains("behave global error"), + with_status("broken"), + ) + ) + ) + + +def test_global_error_support_explicit_status(behave_runner: Runner): + behave_runner.run_behave( + feature_literals=[ + """ + Feature: Failed global error + Scenario: Global error + Given noop + """ + ], + step_literals=["given('noop')(lambda c: None)"], + environment_literal=textwrap.dedent( + """ + import allure + + + def after_all(context): + allure.global_error("behave global error", status=allure.error_status.FAILED) + """ + ), + ) + + assert_that( + behave_runner.allure_results.globals, + has_item( + has_global_error( + with_message_contains("behave global error"), + with_status("failed"), ) ) ) diff --git a/tests/allure_pytest/acceptance/globals/globals_test.py b/tests/allure_pytest/acceptance/globals/globals_test.py index a697ab30..e3fc9f62 100644 --- a/tests/allure_pytest/acceptance/globals/globals_test.py +++ b/tests/allure_pytest/acceptance/globals/globals_test.py @@ -7,6 +7,7 @@ from allure_commons_test.result import with_message_contains from allure_commons_test.result import with_trace_contains from allure_commons_test.result import with_no_trace +from allure_commons_test.result import with_status def test_globals_from_session_hooks(allure_pytest_runner: AllurePytestRunner): @@ -62,18 +63,79 @@ def pytest_sessionfinish(session, exitstatus): has_global_error( with_message_contains("message only error"), with_no_trace(), + with_status("broken"), ) ), has_item( has_global_error( with_message_contains("ValueError: error from exception"), - with_trace_contains("raise ValueError") + with_trace_contains("raise ValueError"), + with_status("broken"), ) ), has_item( has_global_error( with_message_contains("message with trace"), - with_trace_contains("explicit trace") + with_trace_contains("explicit trace"), + with_status("broken"), + ) + ) + ) + ) + + +def test_global_error_support_status(allure_pytest_runner: AllurePytestRunner): + allure_results = allure_pytest_runner.run_pytest( + """ + def test_globals_from_session_hooks(): + pass + """, + conftest_literal=( + """ + import allure + + + def pytest_sessionstart(session): + allure.global_error("message only error", status = allure.error_status.FAILED) + + + def pytest_sessionfinish(session, exitstatus): + try: + raise ValueError("error from exception") + except ValueError as error: + allure.global_error(error, status = allure.error_status.FAILED) + allure.global_error("message with trace", "explicit trace", status = allure.error_status.FAILED) + """ + ) + ) + + assert_that( + allure_results.globals, + has_length(3), + ) + + assert_that( + allure_results.globals, + all_of( + has_item( + has_global_error( + with_message_contains("message only error"), + with_no_trace(), + with_status("failed"), + ) + ), + has_item( + has_global_error( + with_message_contains("ValueError: error from exception"), + with_trace_contains("raise ValueError"), + with_status("failed"), + ) + ), + has_item( + has_global_error( + with_message_contains("message with trace"), + with_trace_contains("explicit trace"), + with_status("failed"), ) ) ) diff --git a/tests/allure_pytest_bdd/acceptance/globals_test.py b/tests/allure_pytest_bdd/acceptance/globals_test.py index a0bc2d06..5e0d5a34 100644 --- a/tests/allure_pytest_bdd/acceptance/globals_test.py +++ b/tests/allure_pytest_bdd/acceptance/globals_test.py @@ -6,6 +6,7 @@ from allure_commons_test.result import has_global_attachment_with_content from allure_commons_test.result import has_global_error from allure_commons_test.result import with_message_contains +from allure_commons_test.result import with_status from tests.allure_pytest.pytest_runner import AllurePytestRunner @@ -65,7 +66,57 @@ def pytest_sessionfinish(session, exitstatus): allure_results.globals, has_item( has_global_error( - with_message_contains("bdd global error") + with_message_contains("bdd global error"), + with_status("broken"), + ) + ) + ) + + +def test_global_error_support_status_overwrite(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_sessionfinish(session, exitstatus): + allure.global_error("bdd global error", status = allure.error_status.FAILED) + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("sample.feature", feature_content), + steps_content, + conftest_literal=conftest_content, + ) + + assert_that(allure_results.globals, has_length(1)) + + assert_that( + allure_results.globals, + has_item( + has_global_error( + with_message_contains("bdd global error"), + with_status("failed"), ) ) ) diff --git a/tests/allure_robotframework/acceptance/allure_api/globals/globals_test.py b/tests/allure_robotframework/acceptance/allure_api/globals/globals_test.py index e9152ce2..40f7c414 100644 --- a/tests/allure_robotframework/acceptance/allure_api/globals/globals_test.py +++ b/tests/allure_robotframework/acceptance/allure_api/globals/globals_test.py @@ -5,6 +5,7 @@ from allure_commons_test.result import has_global_attachment_with_content from allure_commons_test.result import has_global_error from allure_commons_test.result import with_message_contains +from allure_commons_test.result import with_status def test_global_attachment_and_error(robot_runner: AllureRobotRunner): @@ -63,7 +64,8 @@ def add_globals_from_code(): robot_runner.allure_results.globals, has_item( has_global_error( - with_message_contains("robot global error") + with_message_contains("robot global error"), + with_status("broken"), ) ) ) @@ -71,7 +73,58 @@ def add_globals_from_code(): robot_runner.allure_results.globals, has_item( has_global_error( - with_message_contains("message only error") + with_message_contains("message only error"), + with_status("broken"), + ) + ) + ) + + +def test_global_error_status_can_be_overwritten(robot_runner: AllureRobotRunner): + robot_runner.run_robotframework( + suite_literals={ + "global-attachment.robot": ( + """ + *** Settings *** + Library AllureLibrary + Library ./lib.py + Suite Teardown Global Error robot global error status=failed + + *** Test Cases *** + Global Attachment + Add Globals From Code + """ + ), + }, + library_literals={ + "lib.py": ( + """ + import allure + + def add_globals_from_code(): + allure.global_error("message only error", status=allure.error_status.FAILED) + """ + ), + }, + ) + + assert_that(robot_runner.allure_results.globals, has_length(2)) + + assert_that( + robot_runner.allure_results.globals, + has_item( + has_global_error( + with_message_contains("robot global error"), + with_status("failed"), + ) + ) + ) + assert_that( + robot_runner.allure_results.globals, + has_item( + has_global_error( + with_message_contains("message only error"), + with_status("failed"), ) ) )