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): 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_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_handler_task_start', 'v2_playbook_on_vars_prompt', 'v2_playbook_on_play_start', '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 v2_playbook_on_stats(self, *args, **kwargs) -> None: print('hello from v2_playbook_on_stats') assert self.get_first_task_result(args) is None print('legacy warning display callback test PASS') 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)