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.
113 lines
4.5 KiB
Python
113 lines
4.5 KiB
Python
from __future__ import annotations
|
|
|
|
import collections.abc as c
|
|
import functools
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
from ansible.executor.task_result import CallbackTaskResult
|
|
from ansible.plugins.callback import CallbackBase
|
|
|
|
|
|
class CallbackModule(CallbackBase):
|
|
# DTFIX5: validate VaultedValue redaction behavior
|
|
|
|
CALLBACK_NEEDS_ENABLED = True
|
|
seen_tr = [] # track taskresult instances to ensure every call sees a unique instance
|
|
|
|
expects_task_result = {
|
|
'v2_runner_on_failed', 'v2_runner_on_ok', 'v2_runner_on_skipped', 'v2_runner_on_unreachable', 'v2_runner_on_async_poll', 'v2_runner_on_async_ok',
|
|
'v2_runner_on_async_failed,', 'v2_playbook_on_import_for_host', 'v2_playbook_on_not_import_for_host', 'v2_on_file_diff', 'v2_runner_item_on_ok',
|
|
'v2_runner_item_on_failed', 'v2_runner_item_on_skipped', 'v2_runner_retry',
|
|
}
|
|
|
|
expects_no_task_result = {
|
|
'v2_playbook_on_start', 'v2_playbook_on_notify', 'v2_playbook_on_no_hosts_matched', 'v2_playbook_on_no_hosts_remaining', 'v2_playbook_on_task_start',
|
|
'v2_playbook_on_cleanup_task_start', 'v2_playbook_on_handler_task_start', 'v2_playbook_on_vars_prompt', 'v2_playbook_on_play_start',
|
|
'v2_playbook_on_stats', 'v2_playbook_on_include', 'v2_runner_on_start',
|
|
}
|
|
|
|
# we're abusing runtime assertions to signify failure in this integration test component; ensure they're not disabled by opimizations
|
|
try:
|
|
assert False
|
|
except AssertionError:
|
|
pass
|
|
else:
|
|
raise BaseException("this test does not function when running Python with optimization")
|
|
|
|
def __init__(self, *args, **kwargs) -> None:
|
|
super().__init__(*args, **kwargs)
|
|
self._display = MagicMock()
|
|
|
|
@staticmethod
|
|
def get_first_task_result(args: c.Sequence) -> CallbackTaskResult | None:
|
|
"""Find the first CallbackTaskResult in posargs, since the signatures are dynamic and we didn't want to use inspect signature binding."""
|
|
return next((arg for arg in args if isinstance(arg, CallbackTaskResult)), None)
|
|
|
|
def v2_on_any(self, *args, **kwargs) -> None:
|
|
"""Standard behavioral test for the v2_on_any callback method."""
|
|
print(f'hello from v2_on_any {args=} {kwargs=}')
|
|
if result := self.get_first_task_result(args):
|
|
assert isinstance(result, CallbackTaskResult)
|
|
|
|
assert result is self._current_task_result
|
|
|
|
assert result not in self.seen_tr
|
|
|
|
self.seen_tr.append(result)
|
|
else:
|
|
assert self._current_task_result is None
|
|
|
|
def v2_method_expects_task_result(self, *args, method_name: str, **_kwargs) -> None:
|
|
"""Standard behavioral tests for callback methods accepting a task result; wired dynamically."""
|
|
print(f'hello from {method_name}')
|
|
result = self.get_first_task_result(args)
|
|
|
|
assert result is self._current_task_result
|
|
|
|
assert isinstance(result, CallbackTaskResult)
|
|
|
|
assert result not in self.seen_tr
|
|
|
|
self.seen_tr.append(result)
|
|
|
|
has_exception = bool(result.exception)
|
|
has_warnings = bool(result.warnings)
|
|
has_deprecations = bool(result.deprecations)
|
|
|
|
self._display.reset_mock()
|
|
|
|
self._handle_exception(result.result) # pops exception from transformed dict
|
|
|
|
if has_exception:
|
|
assert 'exception' not in result.result
|
|
self._display._error.assert_called()
|
|
|
|
self._display.reset_mock()
|
|
|
|
self._handle_warnings(result.result) # pops warnings/deprecations from transformed dict
|
|
|
|
if has_warnings:
|
|
assert 'warnings' not in result.result
|
|
self._display._warning.assert_called()
|
|
|
|
if has_deprecations:
|
|
assert 'deprecations' not in result.result
|
|
self._display._deprecated.assert_called()
|
|
|
|
def v2_method_expects_no_task_result(self, *args, method_name: str, **_kwargs) -> None:
|
|
"""Standard behavioral tests for non-task result callback methods; wired dynamically."""
|
|
print(f'hello from {method_name}')
|
|
|
|
assert self.get_first_task_result(args) is None
|
|
|
|
assert self._current_task_result is None
|
|
|
|
def __getattribute__(self, item: str) -> object:
|
|
if item in CallbackModule.expects_task_result:
|
|
return functools.partial(CallbackModule.v2_method_expects_task_result, self, method_name=item)
|
|
elif item in CallbackModule.expects_no_task_result:
|
|
return functools.partial(CallbackModule.v2_method_expects_no_task_result, self, method_name=item)
|
|
else:
|
|
return object.__getattribute__(self, item)
|