diff --git a/src/pytest_bdd/compatibility/pytest.py b/src/pytest_bdd/compatibility/pytest.py index 18a79540..e216446b 100644 --- a/src/pytest_bdd/compatibility/pytest.py +++ b/src/pytest_bdd/compatibility/pytest.py @@ -18,6 +18,7 @@ from _pytest.runner import CallInfo from _pytest.terminal import TerminalReporter from pytest import Module as PytestModule +from pytest import fail as _pytest_fail from pytest_bdd.packaging import compare_distribution_version @@ -131,6 +132,14 @@ def get_config_root_path(config: Config) -> Path: return Path(getattr(cast(Config, config), "rootpath" if PYTEST61 else "rootdir")) +def fail(reason, pytrace=True): + __tracebackhide__ = True + if PYTEST7: + return _pytest_fail(reason, pytrace=pytrace) + else: + return _pytest_fail(msg=reason, pytrace=pytrace) + + __all__ = [ "assert_outcomes", "Item", @@ -138,6 +147,7 @@ def get_config_root_path(config: Config) -> Path: "call_fixture_func", "Config", "ExitCode", + "fail", "FixtureDef", "FixtureLookupError", "FixtureRequest", diff --git a/src/pytest_bdd/utils.py b/src/pytest_bdd/utils.py index de8d4ad5..a11b99ed 100644 --- a/src/pytest_bdd/utils.py +++ b/src/pytest_bdd/utils.py @@ -2,17 +2,33 @@ import base64 import pickle import re +import sys from collections import defaultdict -from contextlib import nullcontext, suppress +from contextlib import contextmanager, nullcontext, suppress from enum import Enum from functools import reduce from inspect import getframeinfo, signature from itertools import tee from operator import attrgetter, getitem, itemgetter from sys import _getframe -from typing import TYPE_CHECKING, Any, Callable, Collection, Dict, Mapping, Optional, Sequence, Union, cast - -from pytest_bdd.compatibility.pytest import FixtureDef +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Collection, + Dict, + Mapping, + Optional, + Pattern, + Sequence, + Type, + Union, + cast, +) + +from pytest import raises + +from pytest_bdd.compatibility.pytest import FixtureDef, fail from pytest_bdd.compatibility.typing import Literal, Protocol, runtime_checkable from pytest_bdd.const import ALPHA_REGEX, PYTHON_REPLACE_REGEX @@ -277,3 +293,31 @@ def __next__(self): self._id_counter += 1 get_next_id = __next__ + + +@contextmanager +def doesnt_raise( + expected_exception: Union[Type[Exception], Sequence[Type[Exception]]], + *, + match: Optional[Union[str, Pattern[str]]] = None, + suppress_not_matched=True, +): + """ + + :param expected_exception: Expected exception/s which don't have to be raised; If it raised - test fails + :param match: Message which will be count as failing test. If message is not matched - function passes + :param suppress_not_matched: If specified - all non-matched exceptions will be suppressed + :return: + """ + + try: + yield + except expected_exception: # type:ignore[misc] + ex_type, ex_value, ex_traceback = sys.exc_info() + is_matched = True + if match is not None: + is_matched = bool(re.search(match, f"{ex_value}")) + if is_matched: + fail(f"{ex_value}") + elif not suppress_not_matched: + raise diff --git a/tests/struct_bdd/test_deserialization.py b/tests/struct_bdd/test_deserialization.py index 059922e5..0c51aa88 100644 --- a/tests/struct_bdd/test_deserialization.py +++ b/tests/struct_bdd/test_deserialization.py @@ -9,7 +9,7 @@ from pytest_bdd.model.messages import KeywordType from pytest_bdd.struct_bdd.model import Alternative, Join, Keyword, Node, Step, Table from pytest_bdd.struct_bdd.model_builder import GherkinDocumentBuilder -from pytest_bdd.utils import IdGenerator +from pytest_bdd.utils import IdGenerator, doesnt_raise def test_node_containing_data_load(): @@ -273,14 +273,12 @@ def test_load_simplest_step_with_keyworded_steps(): def test_load_step_with_single_simplest_steps(): - try: + with doesnt_raise(Exception): Step.parse_obj(dict(Steps=[dict(Step=dict())])) - except Exception as e: - raise AssertionError from e def test_node_module_load_for_step(): - try: + with doesnt_raise(Exception): doc = dedent( # language=yaml """\ @@ -300,12 +298,10 @@ def test_node_module_load_for_step(): data = load_yaml(doc, Loader=FullLoader) Step.parse_obj(data) - except Exception as e: - raise AssertionError from e def test_data_load(): - try: + with doesnt_raise(Exception): doc = dedent( # language=yaml """\ @@ -329,12 +325,10 @@ def test_data_load(): data = load_yaml(doc, Loader=FullLoader) Step.parse_obj(data) - except Exception as e: - raise AssertionError from e def test_nested_sub_join_load(): - try: + with doesnt_raise(Exception): doc = dedent( # language=yaml """ @@ -356,12 +350,10 @@ def test_nested_sub_join_load(): data = load_yaml(doc, Loader=FullLoader) Join.parse_obj(data) - except Exception as e: - raise AssertionError from e def test_nested_data_load(): - try: + with doesnt_raise(Exception): doc = dedent( # language=yaml """\ @@ -398,12 +390,10 @@ def test_nested_data_load(): data = load_yaml(doc, Loader=FullLoader) Step.parse_obj(data) - except Exception as e: - raise AssertionError from e def test_nested_examples_load(): - try: + with doesnt_raise(Exception): doc = dedent( # language=yaml """\ @@ -440,8 +430,6 @@ def test_nested_examples_load(): data = load_yaml(doc, Loader=FullLoader) Step.parse_obj(data) - except Exception as e: - raise AssertionError from e def test_tags_steps_examples_load(): @@ -674,7 +662,7 @@ def test_tags_steps_examples_joined_by_value_load(): def test_load_nested_steps(): - try: + with doesnt_raise(Exception): doc = dedent( # language=yaml """\ @@ -697,5 +685,3 @@ def test_load_nested_steps(): data = load_yaml(doc, Loader=FullLoader) Step.parse_obj(data) - except Exception as e: - raise AssertionError from e diff --git a/tests/test_utils.py b/tests/test_utils.py index e7feffbc..a50611d3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,8 +1,9 @@ import pytest +from _pytest.outcomes import Failed from attr import attrib, attrs from pytest import raises -from pytest_bdd.utils import deepattrgetter, setdefaultattr +from pytest_bdd.utils import deepattrgetter, doesnt_raise, setdefaultattr def test_get_attribute(): @@ -207,3 +208,29 @@ class Dumb: with pytest.raises(ValueError): setdefaultattr(Dumb(), "a", value=10, value_factory=lambda: 20) + + +def test_doesnt_raise_fails_test(): + with raises(Failed): + with doesnt_raise(RuntimeError): + raise RuntimeError + + +def test_doesnt_raise_suppress_if_not_match(): + try: + with doesnt_raise(RuntimeError, match="cool"): + raise RuntimeError("nice") + except Exception as e: # pragma: no cover + raise AssertionError from e + + +def test_doesnt_raise_not_suppress_if_not_match_explicitly(): + with raises(RuntimeError, match="nice"): + with doesnt_raise(RuntimeError, match="cool", suppress_not_matched=False): + raise RuntimeError("nice") + + +def test_doesnt_raise_passes_original_exception_if_not_suppressed(): + with raises(ValueError): + with doesnt_raise(RuntimeError, suppress_not_matched=False): + raise ValueError("nice")