Unit tests for the runtime parts of abstract instructions

This commit is contained in:
Emmanuel BENOîT 2022-10-07 17:03:53 +02:00
parent 3c461e84c6
commit 9f4bcd8228
No known key found for this signature in database
GPG key ID: 2356DC6956CF54EF

View file

@ -1,7 +1,7 @@
"""Tests for the instruction base class.""" """Unit tests for the instruction base class."""
import pytest import pytest
from unittest import mock from unittest import mock
from ansible.errors import AnsibleParserError from ansible.errors import AnsibleParserError, AnsibleRuntimeError
from . import reconstructed from . import reconstructed
@ -26,9 +26,15 @@ _INSTR_REPR = _ACTION_NAME + "()"
@pytest.fixture @pytest.fixture
def instr(): def instr():
"""Create a mock instruction suitable for testing.""" """Create a mock instruction suitable for testing."""
return _Instruction( i = _Instruction(mock.MagicMock(), mock.MagicMock(), mock.MagicMock(), _ACTION_NAME)
mock.MagicMock(), mock.MagicMock(), mock.MagicMock(), _ACTION_NAME i._templar.available_variables = None
) return i
@pytest.fixture
def variables():
"""Create a mock variable storage object."""
return mock.MagicMock()
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -428,6 +434,343 @@ class TestParseRunOnce:
assert instr._executed_once is False assert instr._executed_once is False
# ------------------------------------------------------------------------------
class TestRunFor:
"""Tests for the ``run_for()`` method."""
@pytest.fixture
def instr(self):
"""Create a mock instruction suitable for testing."""
instr = _Instruction(
mock.MagicMock(), mock.MagicMock(), mock.MagicMock(), _ACTION_NAME
)
instr.run_iteration = mock.MagicMock()
instr.evaluate_loop = mock.MagicMock()
return instr
def test_run_no_loop(self, instr, variables):
"""Running with no loop set causes ``run_iteration()`` to be called."""
hn = object()
save = object()
instr._save = save
instr._executed_once = None
#
rv = instr.run_for(hn, variables)
#
assert instr._executed_once is None
variables._script_stack_push.assert_called_once_with(save)
variables._script_stack_pop.assert_called_once_with()
instr.run_iteration.assert_called_once_with(hn, variables)
instr.evaluate_loop.assert_not_called()
assert rv is instr.run_iteration.return_value
def test_crash_no_loop(self, instr, variables):
"""If ``run_iteration()`` crashes when there is no loop, the stack \
is popped and the exception is propagated."""
hn = object()
save = object()
instr._save = save
instr._executed_once = None
instr.run_iteration.side_effect = RuntimeError
#
with pytest.raises(RuntimeError):
instr.run_for(hn, variables)
#
assert instr._executed_once is None
variables._script_stack_push.assert_called_once_with(save)
variables._script_stack_pop.assert_called_once_with()
instr.run_iteration.assert_called_once_with(hn, variables)
def test_run_once_first_time(self, instr, variables):
"""The method updates the execution flag and executes the iteration \
if it is set to run once but hasn't been called yet."""
hn = object()
save = object()
instr._save = save
instr._executed_once = False
#
rv = instr.run_for(hn, variables)
#
assert instr._executed_once is True
variables._script_stack_push.assert_called_once_with(save)
variables._script_stack_pop.assert_called_once_with()
instr.run_iteration.assert_called_once_with(hn, variables)
instr.evaluate_loop.assert_not_called()
assert rv is instr.run_iteration.return_value
def test_run_once_already_called(self, instr, variables):
"""The method returns ``True`` but does nothing if it has already been \
called."""
hn = object()
save = object()
instr._save = save
instr._executed_once = True
#
rv = instr.run_for(hn, variables)
#
assert instr._executed_once is True
variables._script_stack_push.assert_not_called()
variables._script_stack_pop.assert_not_called()
instr.run_iteration.assert_not_called()
instr.evaluate_loop.assert_not_called()
assert rv is True
def test_run_loop(self, instr, variables):
"""Running with a loop set causes ``evaluate_loop()`` to be called, \
followed by a call to ``run_iteration()`` for each value it \
returned."""
hn = object()
save = object()
lv = object()
instr._save = save
instr._executed_once = None
instr._loop = [1]
instr._loop_var = lv
instr.evaluate_loop.return_value = (1, 2, 3)
#
rv = instr.run_for(hn, variables)
#
assert instr._executed_once is None
variables._script_stack_push.assert_called_once_with(save)
variables._script_stack_pop.assert_called_once_with()
instr.evaluate_loop.assert_called_once_with(hn, variables)
assert variables.__setitem__.call_args_list == [
mock.call(lv, 1),
mock.call(lv, 2),
mock.call(lv, 3),
]
assert instr.run_iteration.call_args_list == [
mock.call(hn, variables),
mock.call(hn, variables),
mock.call(hn, variables),
]
assert rv is True
def test_run_loop_exit(self, instr, variables):
"""If ``run_iteration()`` returns a falsy value, the loop is interrupted."""
hn = object()
save = object()
lv = object()
instr._save = save
instr._executed_once = None
instr._loop = [1]
instr._loop_var = lv
instr.evaluate_loop.return_value = (1, 2, 3)
instr.run_iteration.return_value = False
#
rv = instr.run_for(hn, variables)
#
assert instr._executed_once is None
variables._script_stack_push.assert_called_once_with(save)
variables._script_stack_pop.assert_called_once_with()
instr.evaluate_loop.assert_called_once_with(hn, variables)
assert variables.__setitem__.call_args_list == [mock.call(lv, 1)]
assert instr.run_iteration.call_args_list == [mock.call(hn, variables)]
assert rv is False
def test_crash_loop(self, instr, variables):
"""If ``run_iteration()`` crashes when there is a loop, the stack \
is popped and the exception is propagated."""
hn = object()
save = object()
lv = object()
instr._save = save
instr._executed_once = None
instr._loop = [1]
instr._loop_var = lv
instr.evaluate_loop.return_value = (1, 2, 3)
instr.run_iteration.side_effect = RuntimeError
#
with pytest.raises(RuntimeError):
instr.run_for(hn, variables)
#
assert instr._executed_once is None
variables._script_stack_push.assert_called_once_with(save)
variables._script_stack_pop.assert_called_once_with()
assert variables.__setitem__.call_args_list == [mock.call(lv, 1)]
assert instr.run_iteration.call_args_list == [mock.call(hn, variables)]
class TestRunIteration:
"""Tests for the ``run_iteration()`` method."""
@pytest.fixture
def instr(self):
"""Create a mock instruction suitable for testing."""
instr = _Instruction(
mock.MagicMock(), mock.MagicMock(), mock.MagicMock(), _ACTION_NAME
)
instr.compute_locals = mock.MagicMock()
instr.evaluate_condition = mock.MagicMock()
instr.execute_action = mock.MagicMock()
return instr
def test_run_cond_false(self, instr, variables):
"""If the condition is not satisfied, ``True`` is returned but the \
action is not executed."""
hn = object()
instr.evaluate_condition.return_value = False
#
rv = instr.run_iteration(hn, variables)
#
instr.compute_locals.assert_called_once_with(variables)
instr.evaluate_condition.assert_called_once_with(hn, variables)
instr.execute_action.assert_not_called()
instr._display.vvvvv.assert_not_called()
assert rv is True
def test_run_cond_true(self, instr, variables):
"""If the condition is satisfied, the action is executed and its \
return value is returned."""
hn = object()
instr.evaluate_condition.return_value = True
#
rv = instr.run_iteration(hn, variables)
#
instr.compute_locals.assert_called_once_with(variables)
instr.evaluate_condition.assert_called_once_with(hn, variables)
instr.execute_action.assert_called_once_with(hn, variables)
instr._display.vvvvv.assert_not_called()
assert rv is instr.execute_action.return_value
def test_run_interrupt(self, instr, variables):
"""If the condition is satisfied and the action returns ``False``, a \
debug message is displayed."""
hn = object()
instr.evaluate_condition.return_value = True
instr.execute_action.return_value = False
#
rv = instr.run_iteration(hn, variables)
#
instr.compute_locals.assert_called_once_with(variables)
instr.evaluate_condition.assert_called_once_with(hn, variables)
instr.execute_action.assert_called_once_with(hn, variables)
instr._display.vvvvv.assert_called_once()
assert rv is False
class TestEvaluateCondition:
"""Tests for the ``evaluate_condition()`` method."""
@pytest.fixture
def instr(self):
"""Create a mock instruction suitable for testing."""
instr = _Instruction(
mock.MagicMock(), mock.MagicMock(), mock.MagicMock(), _ACTION_NAME
)
instr._templar.environment.variable_start_string = "<--"
instr._templar.environment.variable_end_string = "-->"
return instr
@pytest.fixture(autouse=True)
def boolean(self):
"""The Ansible-provided ``boolean`` utility function."""
reconstructed.boolean = mock.MagicMock()
return reconstructed.boolean
def test_no_condition(self, instr, boolean):
"""When there is no condition, ``True`` is returned without the \
template being used."""
instr._condition = None
variables = object()
host_name = object()
#
rv = instr.evaluate_condition(host_name, variables)
#
assert rv is True
instr._templar.template.assert_not_called()
boolean.assert_not_called()
def test_condition_value(self, instr, boolean):
"""When there is a condition, the template is evaluated, and the \
results are converted to a boolean and returned."""
cond = "abc"
variables = object()
host_name = object()
instr._condition = cond
#
rv = instr.evaluate_condition(host_name, variables)
#
assert instr._templar.available_variables is variables
instr._templar.template.assert_called_once_with(
f"<--{cond}-->", disable_lookups=False
)
boolean.assert_called_once_with(instr._templar.template.return_value)
assert rv is boolean.return_value
class TestComputeLocals:
"""Tests for the ``compute_locals()`` method."""
@pytest.fixture
def instr(self):
"""Create a mock instruction suitable for testing."""
instr = _Instruction(
mock.MagicMock(), mock.MagicMock(), mock.MagicMock(), _ACTION_NAME
)
instr._templar.template.side_effect = lambda x: x
return instr
def test_compute_locals(self, instr, variables):
"""Templates are evaluated for each local variable."""
instr._templar.available_variables = None
obj1, obj2, obj3, obj4 = object(), object(), object(), object()
instr._vars = {obj1: obj2, obj3: obj4}
#
instr.compute_locals(variables)
#
assert instr._templar.available_variables is variables
assert instr._templar.template.call_args_list == [
mock.call(obj2),
mock.call(obj4),
]
assert variables.__setitem__.call_args_list == [
mock.call(obj1, obj2),
mock.call(obj3, obj4),
]
class TestEvaluateLoop:
"""Tests for the ``evaluate_loop()`` method."""
def test_list_returned(self, instr):
"""When a list is returned by the template's evaluation, it is \
passed on by the method."""
iv = object()
v = object()
erv = []
instr._loop = iv
instr._templar.available_variables = None
instr._templar.template.return_value = erv
#
rv = instr.evaluate_loop("test", v)
#
assert instr._templar.available_variables is v
instr._templar.template.assert_called_once_with(iv, disable_lookups=False)
assert rv is erv
def test_bad_return_type(self, instr):
"""When something that isn't a list is returned by the template's \
evaluation, an Ansible runtime error occurs."""
iv = object()
v = object()
erv = {}
instr._loop = iv
instr._templar.available_variables = None
instr._templar.template.return_value = erv
#
with pytest.raises(AnsibleRuntimeError):
instr.evaluate_loop("test", v)
#
assert instr._templar.available_variables is v
instr._templar.template.assert_called_once_with(iv, disable_lookups=False)
# ------------------------------------------------------------------------------
class TestParseGroupName: class TestParseGroupName:
"""Tests for the ``parse_group_name()`` helper method.""" """Tests for the ``parse_group_name()`` helper method."""