Pin black version in tox.ini to 23.12.1
[osm/N2VC.git] / n2vc / tests / unit / test_juju_watcher.py
index 41065bf..b9e9e36 100644 (file)
 #     See the License for the specific language governing permissions and
 #     limitations under the License.
 
+import json
+import os
+from time import sleep
 import asynctest
 import asyncio
 
-from unittest import mock, TestCase
-from unittest.mock import Mock
 from n2vc.juju_watcher import JujuModelWatcher, entity_ready, status
 from n2vc.exceptions import EntityInvalidException
 from .utils import FakeN2VC, AsyncMock, Deltas, FakeWatcher
 from juju.application import Application
-from juju.model import Model
+from juju.action import Action
 from juju.annotation import Annotation
+from juju.client._definitions import AllWatcherNextResults
 from juju.machine import Machine
-from juju.action import Action
+from juju.model import Model
+from juju.unit import Unit
+from unittest import mock, TestCase
+from unittest.mock import Mock
 
 
 class JujuWatcherTest(asynctest.TestCase):
@@ -45,6 +50,7 @@ class JujuWatcherTest(asynctest.TestCase):
     def test_model_watcher(self, allwatcher):
         tests = Deltas
         allwatcher.return_value = FakeWatcher()
+        n2vc = AsyncMock()
         for test in tests:
             with self.assertRaises(asyncio.TimeoutError):
                 allwatcher.return_value.delta_to_return = [test.delta]
@@ -55,12 +61,12 @@ class JujuWatcherTest(asynctest.TestCase):
                         test.filter.entity_type,
                         timeout=0,
                         db_dict={"something"},
-                        n2vc=self.n2vc,
+                        n2vc=n2vc,
+                        vca_id=None,
                     )
                 )
 
-            self.assertEqual(self.n2vc.last_written_values, test.db.data)
-            self.n2vc.last_written_values = None
+            n2vc.write_app_status_to_db.assert_called()
 
     @mock.patch("n2vc.juju_watcher.asyncio.wait")
     def test_wait_for(self, wait):
@@ -121,6 +127,167 @@ class EntityReadyTest(TestCase):
         self.assertTrue(isinstance(entity_ready(entity), bool))
 
 
+@mock.patch("n2vc.juju_watcher.client.AllWatcherFacade.from_connection")
+class EntityStateTest(TestCase):
+    def setUp(self):
+        self.model = Model()
+        self.model._connector = mock.MagicMock()
+        self.loop = asyncio.new_event_loop()
+        self.application = Mock(Application)
+        self.upgrade_file = None
+        self.line_number = 1
+
+    def _fetch_next_delta(self):
+        delta = None
+        while delta is None:
+            raw_data = self.upgrade_file.readline()
+            if not raw_data:
+                raise EOFError("Log file is out of events")
+            try:
+                delta = json.loads(raw_data)
+            except ValueError:
+                continue
+
+        if delta[0] == "unit":
+            if delta[2]["life"] == "dead":
+                # Remove the unit from the application
+                for unit in self.application.units:
+                    if unit.entity_id == delta[2]["name"]:
+                        self.application.units.remove(unit)
+            else:
+                unit_present = False
+                for unit in self.application.units:
+                    if unit.entity_id == delta[2]["name"]:
+                        unit_present = True
+
+                if not unit_present:
+                    print("Application gets a new unit: {}".format(delta[2]["name"]))
+                    unit = Mock(Unit)
+                    unit.entity_id = delta[2]["name"]
+                    unit.entity_type = "unit"
+                    self.application.units.append(unit)
+
+        print("{}  {}".format(self.line_number, delta))
+        self.line_number = self.line_number + 1
+
+        return AllWatcherNextResults(
+            deltas=[
+                delta,
+            ]
+        )
+
+    def _ensure_state(self, filename, mock_all_watcher):
+        with open(
+            os.path.join(os.path.dirname(__file__), "testdata", filename),
+            "r",
+        ) as self.upgrade_file:
+            all_changes = AsyncMock()
+            all_changes.Next.side_effect = self._fetch_next_delta
+            mock_all_watcher.return_value = all_changes
+
+            self.loop.run_until_complete(
+                JujuModelWatcher.ensure_units_idle(
+                    model=self.model, application=self.application
+                )
+            )
+
+            with self.assertRaises(EOFError, msg="Not all events consumed"):
+                change = self._fetch_next_delta()
+                print(change.deltas[0].deltas)
+
+    def _slow_changes(self):
+        sleep(0.1)
+        return AllWatcherNextResults(
+            deltas=[
+                json.loads(
+                    """["unit","change",
+                {
+                    "name": "app-vnf-7a49ace2b6-z0/2",
+                    "application": "app-vnf-7a49ace2b6-z0",
+                    "workload-status": {
+                        "current": "active",
+                        "message": "",
+                        "since": "2022-04-26T18:50:27.579802723Z"},
+                    "agent-status": {
+                        "current": "idle",
+                        "message": "",
+                        "since": "2022-04-26T18:50:28.592142816Z"}
+                }]"""
+                ),
+            ]
+        )
+
+    def test_timeout(self, mock_all_watcher):
+        unit1 = Mock(Unit)
+        unit1.entity_id = "app-vnf-7a49ace2b6-z0/0"
+        unit1.entity_type = "unit"
+        self.application.units = [
+            unit1,
+        ]
+
+        all_changes = AsyncMock()
+        all_changes.Next.side_effect = self._slow_changes
+        mock_all_watcher.return_value = all_changes
+
+        with self.assertRaises(TimeoutError):
+            self.loop.run_until_complete(
+                JujuModelWatcher.wait_for_units_idle(
+                    model=self.model, application=self.application, timeout=0.01
+                )
+            )
+
+    def test_machine_unit_upgrade(self, mock_all_watcher):
+        unit1 = Mock(Unit)
+        unit1.entity_id = "app-vnf-7a49ace2b6-z0/0"
+        unit1.entity_type = "unit"
+        unit2 = Mock(Unit)
+        unit2.entity_id = "app-vnf-7a49ace2b6-z0/1"
+        unit2.entity_type = "unit"
+        unit3 = Mock(Unit)
+        unit3.entity_id = "app-vnf-7a49ace2b6-z0/2"
+        unit3.entity_type = "unit"
+
+        self.application.units = [unit1, unit2, unit3]
+
+        self._ensure_state("upgrade-machine.log", mock_all_watcher)
+
+    def test_operator_upgrade(self, mock_all_watcher):
+        unit1 = Mock(Unit)
+        unit1.entity_id = "sshproxy/0"
+        unit1.entity_type = "unit"
+        self.application.units = [
+            unit1,
+        ]
+        self._ensure_state("upgrade-operator.log", mock_all_watcher)
+
+    def test_podspec_stateful_upgrade(self, mock_all_watcher):
+        unit1 = Mock(Unit)
+        unit1.entity_id = "mongodb/0"
+        unit1.entity_type = "unit"
+        self.application.units = [
+            unit1,
+        ]
+        self._ensure_state("upgrade-podspec-stateful.log", mock_all_watcher)
+
+    def test_podspec_stateless_upgrade(self, mock_all_watcher):
+        unit1 = Mock(Unit)
+        unit1.entity_id = "lcm/9"
+        unit1.entity_type = "unit"
+        self.application.units = [
+            unit1,
+        ]
+        self._ensure_state("upgrade-podspec-stateless.log", mock_all_watcher)
+
+    def test_sidecar_upgrade(self, mock_all_watcher):
+        unit1 = Mock(Unit)
+        unit1.entity_id = "kafka/0"
+        unit1.entity_type = "unit"
+        self.application.units = [
+            unit1,
+        ]
+        self._ensure_state("upgrade-sidecar.log", mock_all_watcher)
+
+
 class StatusTest(TestCase):
     def setUp(self):
         self.model = Model()
@@ -142,6 +309,7 @@ class StatusTest(TestCase):
         self.assertTrue(isinstance(value, str))
 
 
+@asynctest.mock.patch("asyncio.sleep")
 class WaitForModelTest(asynctest.TestCase):
     @asynctest.mock.patch("juju.client.connector.Connector.connect")
     def setUp(self, mock_connect=None):
@@ -149,7 +317,7 @@ class WaitForModelTest(asynctest.TestCase):
         self.model = Model()
 
     @asynctest.mock.patch("juju.model.Model.block_until")
-    def test_wait_for_model(self, mock_block_until):
+    def test_wait_for_model(self, mock_block_until, mock_sleep):
         self.loop.run_until_complete(
             JujuModelWatcher.wait_for_model(self.model, timeout=None)
         )
@@ -157,7 +325,7 @@ class WaitForModelTest(asynctest.TestCase):
 
     @asynctest.mock.patch("asyncio.ensure_future")
     @asynctest.mock.patch("asyncio.wait")
-    def test_wait_for_model_exception(self, mock_wait, mock_ensure_future):
+    def test_wait_for_model_exception(self, mock_wait, mock_ensure_future, mock_sleep):
         task = Mock()
         mock_ensure_future.return_value = task
         mock_wait.side_effect = Exception