From bff1e6bc3b0b223ec995427609d488cc24b82449 Mon Sep 17 00:00:00 2001 From: Stefan Hoelzl <1478183+stefanhoelzl@users.noreply.github.com> Date: Thu, 9 Dec 2021 22:10:33 +0000 Subject: [PATCH] [feature] detect cycles in profile definitions --- conftest.py | 2 +- pytest_profiles/profile.py | 39 ++++++++++++++++++++++++++++++-------- tests/test_profile.py | 20 ++++++++++++++++--- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/conftest.py b/conftest.py index e55ec5a..b2254d8 100644 --- a/conftest.py +++ b/conftest.py @@ -68,6 +68,6 @@ def mccabe(config: Config) -> None: """profile for mccabe code complexity""" config.option.mccabe = True try: - config.addinivalue_line("mccabe-complexity", "3") + config.addinivalue_line("mccabe-complexity", "4") except ValueError: pass diff --git a/pytest_profiles/profile.py b/pytest_profiles/profile.py index 5355bf6..1d19b7c 100644 --- a/pytest_profiles/profile.py +++ b/pytest_profiles/profile.py @@ -16,6 +16,25 @@ from _pytest.config import Config # pylint: disable=protected-access + +class PytestProfilesException(Exception): + """Base for pytest profiles exceptions.""" + + +class UnknownProfiles(PytestProfilesException): + """Exception for using a unknown profile.""" + + def __init__(self, unkonwn_profiles: Iterable[str]) -> None: + super().__init__(f"unregistered profiles used: {', '.join(unkonwn_profiles)}") + + +class ProfileCycleDetected(PytestProfilesException): + """Exception when an cycle in the profile definitions is detected.""" + + def __init__(self, cycle: Iterable[str]) -> None: + super().__init__(f"profile cycle detected: {' -> '.join(cycle)}") + + RegisteredProfiles: MutableMapping[str, "Profile"] = OrderedDict() @@ -81,20 +100,24 @@ def resolve_profiles( ) deduplicated = OrderedDict((n, None) for n in with_dependecies).keys() - _check_for_unregistered_profiles(deduplicated) + _check_for_unknown_profiles(deduplicated) for profile_name in deduplicated: yield RegisteredProfiles[profile_name] -def _with_dependencies(profile_name: str) -> Generator[str, None, None]: +def _with_dependencies( + profile_name: str, chain: Optional[List[str]] = None +) -> Generator[str, None, None]: if profile_name in RegisteredProfiles: + chain = chain or [] + if profile_name in chain: + raise ProfileCycleDetected([*chain, profile_name]) for dependency in RegisteredProfiles[profile_name].uses or []: - yield from _with_dependencies(dependency) - yield dependency + yield from _with_dependencies(dependency, chain=[*chain, profile_name]) yield profile_name -def _check_for_unregistered_profiles(profile_names: Iterable[str]) -> None: - unregistered = [n for n in profile_names if n not in RegisteredProfiles] - if unregistered: - raise ValueError(f"unregistered profiles used: {', '.join(unregistered)}") +def _check_for_unknown_profiles(profile_names: Iterable[str]) -> None: + unknown = [n for n in profile_names if n not in RegisteredProfiles] + if unknown: + raise UnknownProfiles(unknown) diff --git a/tests/test_profile.py b/tests/test_profile.py index a486d95..2516000 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -2,7 +2,13 @@ import pytest from _pytest.config import Config # pylint: disable=protected-access -from pytest_profiles.profile import RegisteredProfiles, profile, resolve_profiles +from pytest_profiles.profile import ( + ProfileCycleDetected, + RegisteredProfiles, + UnknownProfiles, + profile, + resolve_profiles, +) def test_create_and_apply(pytester: pytest.Pytester) -> None: @@ -67,6 +73,14 @@ def test_resolve_profiles_keep_order() -> None: assert list(resolve_profiles(profiles=["first"])) == [first, second] -def test_resolve_profiles_value_error_on_unregistered_profile() -> None: - with pytest.raises(ValueError): +def test_resolve_profiles_value_error_on_unknown_profile() -> None: + with pytest.raises(UnknownProfiles): + list(resolve_profiles(profiles=["first"])) + + +def test_resolve_profiles_detect_cycles() -> None: + profile(name="first", uses="second")(lambda c: None) + profile(name="second", uses="first")(lambda c: None) + + with pytest.raises(ProfileCycleDetected): list(resolve_profiles(profiles=["first"]))