diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..459e4fa --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +ansible>=6.0.0 +pytest>=7.1,<7.2 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..c37fb90 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,10 @@ +"""Tests for the ``reconstructed`` plugin.""" +import os +import sys + +sys.path.insert( + 0, + os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "inventory_plugins")), +) + +import reconstructed diff --git a/tests/test_variables.py b/tests/test_variables.py new file mode 100644 index 0000000..c7e974d --- /dev/null +++ b/tests/test_variables.py @@ -0,0 +1,296 @@ +"""Tests for the variable storage / cache.""" +import pytest + +from . import reconstructed + + +class TestBasics: + """Test basic access to the store.""" + + @pytest.fixture + def host_vars(self): + """A set of fake host vars to use.""" + return { + "var1": "value1", + "var2": "value2", + } + + @pytest.fixture + def store(self, host_vars): + """A store initialized with the host variables above.""" + return reconstructed.VariableStorage(host_vars) + + def test_init(self, host_vars, store): + """Reference to host vars is copied, cache is initialized, stack is empty.""" + assert store._host_vars is host_vars + assert store._cache == host_vars + assert store._cache is not host_vars + assert len(store._script_vars) == 0 + assert len(store._script_stack) == 0 + + def test_read_hostvar(self, host_vars, store): + """Host vars can be read from the store.""" + for k, v in host_vars.items(): + assert store[k] == host_vars[k] + + def test_del_hostvars(self, host_vars, store): + """Host variables cannot be deleted.""" + for k in host_vars.keys(): + with pytest.raises(KeyError): + del store[k] + assert k in store._host_vars + assert k in store._cache + assert store[k] == host_vars[k] + + def test_write_local(self, host_vars, store): + """Variables can be written and read back.""" + k, v = "local", "valuel" + assert k not in store + store[k] = v + assert k in store + assert store[k] == v + assert k not in host_vars + assert k in store._script_vars + assert store._script_vars[k] == v + assert k in store._cache + assert store._cache[k] == v + + def test_del_local(self, host_vars, store): + """Local variables can be deleted.""" + k, v = "local", "valuel" + assert k not in store + store[k] = v + del store[k] + assert k not in store + assert k not in host_vars + assert k not in store._script_vars + assert k not in store._cache + + def test_local_hides_hostvar(self, host_vars, store): + """Local variables can hide host variables with the same name.""" + k = tuple(host_vars.keys())[0] + initial = host_vars[k] + v = initial + "nope" + store[k] = v + assert k in store + assert store[k] == v + assert host_vars[k] == initial + assert k in store._script_vars + assert store._script_vars[k] == v + assert k in store._cache + assert store._cache[k] == v + + def test_del_unhides_hostvar(self, host_vars, store): + """Deleting a local that hides a host vars makes the host var visible again.""" + k = tuple(host_vars.keys())[0] + initial = host_vars[k] + v = initial + "nope" + store[k] = v + del store[k] + assert k in store + assert store[k] == initial + assert host_vars[k] == initial + assert k not in store._script_vars + assert k in store._cache + assert store._cache[k] == initial + + +class TestIterators: + """Tests for the various iterator methods.""" + + @pytest.fixture + def host_vars(self): + """A set of fake host vars to use.""" + return { + "var1": "value1", + "var2": "value2", + } + + @pytest.fixture + def local_vars(self, host_vars): + """A set of local vars, one of which overrides a host var.""" + k = tuple(host_vars.keys())[0] + return { + k: "not " + host_vars[k], + "varl": "valuel", + } + + @pytest.fixture + def store(self, host_vars, local_vars): + """A store initialized with both host variables and locals.""" + store = reconstructed.VariableStorage(host_vars) + for k, v in local_vars.items(): + store[k] = v + return store + + def test_keys(self, host_vars, local_vars, store): + """A store's keys are the union of its host and local variable names.""" + expected = set(host_vars.keys()) | set(local_vars.keys()) + actual = set(store.keys()) + assert expected == actual + + def test_values(self, host_vars, local_vars, store): + """A store's values correspond to the ones in host and local variables, \ + with the latter taking precedence.""" + expected = dict(host_vars) + expected.update(local_vars) + assert tuple(expected.values()) == tuple(store.values()) + + def test_items(self, host_vars, local_vars, store): + """A store's items correspond to the ones in host and local variables, \ + with the latter taking precedence.""" + expected = dict(host_vars) + expected.update(local_vars) + assert tuple(expected.items()) == tuple(store.items()) + + def test_iter(self, host_vars, local_vars, store): + """Iterating over a store is equivalent to getting its keys.""" + expected = set(host_vars.keys()) | set(local_vars.keys()) + actual = set(k for k in store) + assert expected == actual + + +class TestStack: + """Tests for the stack features of the variable storage class.""" + + @pytest.fixture + def host_vars(self): + """A set of fake host vars to use.""" + return { + "var1": ["value1"], + } + + @pytest.fixture + def local_vars(self): + """A set of fake local variables.""" + return { + "varl1": ["valuel1"], + } + + @pytest.fixture + def store(self, host_vars, local_vars): + """A store initialized with both host variables and locals.""" + store = reconstructed.VariableStorage(host_vars) + for k, v in local_vars.items(): + store[k] = v + return store + + def test_push_empty(self, store): + """Test pushing nothing.""" + store._script_stack_push(()) + assert len(store._script_stack) == 1 + assert store._script_stack[0] == {} + + def test_push_missing(self, store): + """Test pushing a variable that is not defined.""" + v = "nope" + assert v not in store._script_vars + assert v not in store._host_vars + store._script_stack_push((v,)) + assert len(store._script_stack) == 1 + assert store._script_stack[0] == {v: (False, None)} + + def test_push_host(self, store, host_vars): + """Test pushing a single, existing host var.""" + v = "var1" + assert v in store._host_vars + store._script_stack_push((v,)) + assert len(store._script_stack) == 1 + assert store._script_stack[0] == {v: (False, None)} + + def test_push_local(self, store): + """Test pushing a single, existing local var.""" + v = "varl1" + assert v in store._script_vars + store._script_stack_push((v,)) + assert len(store._script_stack) == 1 + assert store._script_stack[0] == {v: (True, store._script_vars[v])} + assert store._script_stack[0][v][1] is not store._script_vars[v] + + def test_pop_empty(self, store): + """Test poping an empty stack entry.""" + old_cache = store._cache + store._script_stack.append({}) + store._script_stack_pop() + assert len(store._script_stack) == 0 + assert store._cache is old_cache + + def test_pop_missing(self, store): + """Test poping a variable that is not defined.""" + old_cache = store._cache + v = "nope" + assert v not in store._script_vars + assert v not in store._host_vars + store._script_stack.append({v: (False, None)}) + store._script_stack_pop() + assert len(store._script_stack) == 0 + assert store._cache is old_cache + + def test_pop_unchanged_host(self, store): + """Test poping a host variable that was not overridden.""" + old_cache = store._cache + v = "var1" + assert v in store._host_vars + assert v not in store._script_vars + store._script_stack.append({v: (False, None)}) + store._script_stack_pop() + assert len(store._script_stack) == 0 + assert store._cache is old_cache + + def test_pop_overridden_host(self, store): + """Test poping a host variable that was overridden.""" + old_cache = store._cache + v_name = "var1" + v_value = "test" + assert v_name in store._host_vars + assert v_name not in store._script_vars + store._script_stack.append({v_name: (False, None)}) + store._script_vars[v_name] = v_value + store._cache[v_name] = v_value + assert store[v_name] == v_value + + store._script_stack_pop() + + assert len(store._script_stack) == 0 + assert store._cache is not old_cache + assert store._cache[v_name] == store._host_vars[v_name] + assert v_name not in store._script_vars + + def test_pop_unchanged_local(self, store): + """Test poping a script variable that was not changed.""" + old_cache = store._cache + v_name = "varl1" + assert v_name not in store._host_vars + assert v_name in store._script_vars + v_initial = store._script_vars[v_name] + store._script_stack.append({v_name: (True, v_initial)}) + + store._script_stack_pop() + + assert len(store._script_stack) == 0 + assert store._cache is not old_cache + assert v_name in store._cache + assert store._cache[v_name] is v_initial + assert v_name in store._script_vars + assert store._script_vars[v_name] is v_initial + + def test_pop_modified_local(self, store): + """Test poping a script variable that was modified.""" + old_cache = store._cache + v_name = "varl1" + assert v_name not in store._host_vars + assert v_name in store._script_vars + v_initial = store._script_vars[v_name] + v_new = ["no"] + store._script_vars[v_name] = v_new + store._cache[v_name] = v_new + store._script_stack.append({v_name: (True, v_initial)}) + + store._script_stack_pop() + + assert len(store._script_stack) == 0 + assert store._cache is not old_cache + assert v_name in store._cache + assert store._cache[v_name] is v_initial + assert v_name in store._script_vars + assert store._script_vars[v_name] is v_initial