From 3d0fcb76d234aabe5c7daedf6cd5648dee9b57af Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Emmanuel=20Beno=C3=AEt?= <tseeker@nocternity.net>
Date: Sun, 18 Sep 2022 12:15:03 +0200
Subject: [PATCH] Unit tests for the variable storage class

---
 requirements-dev.txt    |   2 +
 tests/__init__.py       |  10 ++
 tests/test_variables.py | 296 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 308 insertions(+)
 create mode 100644 requirements-dev.txt
 create mode 100644 tests/__init__.py
 create mode 100644 tests/test_variables.py

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