mirror of https://github.com/ansible/ansible.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
138 lines
5.5 KiB
Python
138 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
import pytest
|
|
import pytest_mock
|
|
|
|
from ansible.constants import config
|
|
from ansible.errors import AnsibleUndefinedConfigEntry
|
|
from ansible._internal._errors._handler import ErrorHandler, ErrorAction, Skippable, _SkipException
|
|
from ansible.utils.display import Display
|
|
|
|
|
|
def test_skippable_ignore_skips_body() -> None:
|
|
"""Verify that `skip_on_ignore=True` skips the body within the context manager when `action=ErrorAction.IGNORE`."""
|
|
body_ran = False
|
|
assert not body_ran # satisfy static analysis which assumes the context manager body will run
|
|
|
|
with Skippable, ErrorHandler(ErrorAction.IGNORE).handle(Exception, skip_on_ignore=True):
|
|
body_ran = True
|
|
|
|
assert not body_ran
|
|
|
|
|
|
def test_skippable_without_skip_on_ignore() -> None:
|
|
"""
|
|
Verify using `Skippable` without invoking a handler with `skip_on_ignore=True` will fail.
|
|
This protects against accidental use of `Skippable` by itself, or forgetting to use `skip_on_ignore=True` -- both of which have no effect.
|
|
"""
|
|
body_ran = False
|
|
assert not body_ran # satisfy static analysis which assumes the context manager body will run
|
|
|
|
with pytest.raises(RuntimeError) as err:
|
|
with Skippable:
|
|
body_ran = True
|
|
|
|
assert body_ran
|
|
assert 'handler was never invoked' in str(err.value)
|
|
|
|
|
|
def test_skippable_non_skip_exception() -> None:
|
|
"""Verify that `Skippable` does not interfere with exceptions."""
|
|
ex_to_raise = RuntimeError('let me through')
|
|
|
|
with pytest.raises(RuntimeError) as err:
|
|
with Skippable:
|
|
raise ex_to_raise
|
|
|
|
assert err.value is ex_to_raise
|
|
|
|
|
|
@pytest.mark.parametrize("error_action", (ErrorAction.IGNORE, ErrorAction.WARNING, ErrorAction.ERROR))
|
|
def test_skip_on_ignore_missing_skippable(error_action: ErrorAction) -> None:
|
|
"""Verify that a `_SkipException` is raised when `skip_on_ignore=True` and no `Skippable` context was used to suppress it."""
|
|
body_ran = False
|
|
assert not body_ran # satisfy static analysis which assumes the context manager body will run
|
|
|
|
with pytest.raises(_SkipException):
|
|
with ErrorHandler(error_action).handle(Exception, skip_on_ignore=True):
|
|
body_ran = True
|
|
|
|
if error_action is ErrorAction.IGNORE:
|
|
assert not body_ran
|
|
else:
|
|
assert body_ran
|
|
|
|
|
|
@pytest.mark.parametrize("exception_type", (RuntimeError, NotImplementedError))
|
|
def test_ignore_success(exception_type: type[Exception]) -> None:
|
|
"""Verify that `ErrorAction.IGNORE` suppresses the specified exception types."""
|
|
body_ran = False
|
|
assert not body_ran # satisfy static analysis which assumes the context manager body will run
|
|
|
|
with ErrorHandler(ErrorAction.IGNORE).handle(RuntimeError, NotImplementedError):
|
|
body_ran = True
|
|
raise exception_type('should be ignored')
|
|
|
|
assert body_ran
|
|
|
|
|
|
def test_ignore_passes_other_exceptions() -> None:
|
|
"""Verify that `ErrorAction.IGNORE` does not suppress exception types not passed to `handle`."""
|
|
with pytest.raises(NotImplementedError):
|
|
with ErrorHandler(ErrorAction.IGNORE).handle(TypeError, ValueError):
|
|
raise NotImplementedError()
|
|
|
|
|
|
@pytest.mark.parametrize("exception_type", (RuntimeError, NotImplementedError))
|
|
def test_warn_success(exception_type: type[Exception], mocker: pytest_mock.MockerFixture) -> None:
|
|
"""Verify that `ErrorAction.WARNING` eats the specified error type and calls `error_as_warning` with the exception instance raised."""
|
|
eaw = mocker.patch.object(Display(), 'error_as_warning')
|
|
with ErrorHandler(ErrorAction.WARNING).handle(RuntimeError, NotImplementedError):
|
|
raise exception_type()
|
|
|
|
assert isinstance(eaw.call_args.kwargs['exception'], exception_type)
|
|
|
|
|
|
def test_warn_passes_other_exceptions(mocker: pytest_mock.MockerFixture) -> None:
|
|
"""Verify that `ErrorAction.WARNING` does not suppress exception types not passed to `handle`, and that `error_as_warning` is not called for them."""
|
|
eaw = mocker.patch.object(Display(), 'error_as_warning')
|
|
|
|
with pytest.raises(NotImplementedError):
|
|
with ErrorHandler(ErrorAction.WARNING).handle(TypeError, ValueError):
|
|
raise NotImplementedError()
|
|
|
|
assert not eaw.called
|
|
|
|
|
|
@pytest.mark.parametrize("exception_type", (AttributeError, NotImplementedError, ValueError))
|
|
def test_fail(exception_type: type[Exception]) -> None:
|
|
"""Verify that `ErrorAction.ERROR` passes through all exception types, regardless of what was passed to `handle`."""
|
|
with pytest.raises(exception_type):
|
|
with ErrorHandler(ErrorAction.ERROR).handle(AttributeError, NotImplementedError):
|
|
raise exception_type()
|
|
|
|
|
|
def test_no_exceptions_to_handle():
|
|
"""Verify that passing no exceptions to `handle` fails."""
|
|
with pytest.raises(ValueError):
|
|
with ErrorHandler(ErrorAction.IGNORE).handle():
|
|
pass
|
|
|
|
|
|
@pytest.mark.parametrize("value", ('ignore', 'warning', 'error'))
|
|
def test_from_config_env_success(value: str, mocker: pytest_mock.MockerFixture) -> None:
|
|
"""Verify that `from_config` correctly creates handlers with the requested error action config string."""
|
|
mocker.patch.dict(os.environ, dict(_ANSIBLE_CALLBACK_DISPATCH_ERROR_BEHAVIOR=value))
|
|
|
|
assert config.get_config_value("_CALLBACK_DISPATCH_ERROR_BEHAVIOR") == value
|
|
eh = ErrorHandler.from_config("_CALLBACK_DISPATCH_ERROR_BEHAVIOR")
|
|
assert eh.action == ErrorAction[value.upper()]
|
|
|
|
|
|
def test_from_config_fail() -> None:
|
|
"""Verify that `from_config` fails on an invalid config entry name."""
|
|
with pytest.raises(AnsibleUndefinedConfigEntry):
|
|
ErrorHandler.from_config("invalid")
|