Parcourir la source

Add unit tests for Coriolis Scheduler RPC Server

Sergiu Miclea il y a 3 ans
Parent
commit
1d7e51ad98

+ 0 - 0
coriolis/tests/scheduler/__init__.py


+ 0 - 0
coriolis/tests/scheduler/rpc/__init__.py


+ 224 - 0
coriolis/tests/scheduler/rpc/data/get_workers_for_specs_config.yaml

@@ -0,0 +1,224 @@
+## ENABLED TESTS ##
+
+# no filters, but services available
+- config: 
+    services_db:
+      - id: 1
+        topic: coriolis_worker
+      - id: 2
+        topic: coriolis_scheduler
+      - id: 3
+        topic: coriolis_worker
+  expected_result: [1, 3]
+  expected_exception: ~
+
+# different topic and enabled combinations
+- config: 
+    enabled: true
+    services_db:
+      - id: 1
+        topic: coriolis_worker
+        enabled: true
+      - id: 2
+        topic: coriolis_worker
+        enabled: false
+      - id: 3
+        topic: coriolis_worker
+        enabled: true
+      - id: 4
+        topic: coriolis_scheduler
+        enabled: true
+  expected_result: [1, 3]
+  expected_exception: ~
+
+# REGIONS TESTS ##
+
+- config:
+    region_sets: [[region_1, region_2], [region_3]]
+    regions_db:
+      - id: region_1
+        enabled: true
+      - id: region_2
+        enabled: false
+      - id: region_3
+        enabled: true
+    services_db:
+      - id: 1
+        topic: coriolis_worker
+        mapped_regions: 
+          - id: region_1
+          - id: region_3
+      - id: 2
+        topic: coriolis_worker
+        mapped_regions: 
+          - id: region_2
+          - id: region_3
+      # region_3 not mapped
+      - id: 3
+        topic: coriolis_worker
+        mapped_regions: 
+          - id: region_2
+      - id: 4
+        topic: coriolis_worker
+        mapped_regions: 
+          - id: region_1
+          - id: region_2
+          - id: region_3
+      - id: 5
+        topic: coriolis_worker
+        mapped_regions: 
+          - id: invalid_region
+  expected_result: [1, 2, 4]
+  expected_exception: ~
+
+# region_3 is disabled in DB
+- config:
+    region_sets: [[region_1, region_2], [region_3]]
+    regions_db: ~
+    services_db:
+      - id: 1
+        topic: coriolis_worker
+        mapped_regions: 
+          - id: region_1
+          - id: region_3
+  expected_result: ~
+  expected_exception: NoSuitableRegionError
+
+## PROVIDERS TESTS ##
+
+- config:
+    provider_requirements:
+      provider_1: [1, 2, 16]
+      provider_2: [1, 2]
+      provider_3: [1, 32]
+    services_db:
+      - id: 1
+        topic: coriolis_worker
+        providers: 
+          provider_1: 
+            types: [1, 2, 16]
+          provider_2: 
+            types: [1, 2, 16]
+          provider_3: 
+            types: [1, 2, 16, 32]
+      # 2 is missing provider_3 with 32
+      - id: 2
+        topic: coriolis_worker
+        providers: 
+          provider_1: 
+            types: [1, 2, 16]
+          provider_2: 
+            types: [1, 2, 16]
+          provider_3: 
+            types: [1, 2, 16]
+      - id: 3
+        topic: coriolis_worker
+        providers: 
+          provider_1: 
+            types: [1, 2, 16, 32]
+          provider_2: 
+            types: [1, 2, 16, 32]
+          provider_3: 
+            types: [1, 2, 16, 32, 64]
+  expected_result: [1, 3]
+  expected_exception: ~
+
+## ALL TOGETHER: ENABLED, REGIONS AND PROVIDER ##
+
+- config:
+    enabled: true
+    region_sets: [[region_1, region_2], [region_3]]
+    provider_requirements:
+      provider_1: [1, 2, 16]
+      provider_2: [1, 2]
+      provider_3: [1, 32]
+    regions_db:
+      - id: region_1
+        enabled: true
+      - id: region_2
+        enabled: false
+      - id: region_3
+        enabled: true
+    services_db:
+      - id: 1
+        topic: coriolis_worker
+        enabled: true
+        mapped_regions: 
+          - id: region_1
+          - id: region_3
+        providers: 
+          provider_1: 
+            types: [1, 2, 16]
+          provider_2: 
+            types: [1, 2, 16]
+          provider_3: 
+            types: [1, 2, 16, 32]
+      # 2 is missing provider_3 with 32
+      - id: 2
+        topic: coriolis_worker
+        enabled: true
+        mapped_regions: 
+          - id: region_2
+          - id: region_3
+        providers: 
+          provider_1: 
+            types: [1, 2, 16]
+          provider_2: 
+            types: [1, 2, 16]
+          provider_3: 
+            types: [1, 2, 16]
+      - id: 3
+        topic: coriolis_worker
+        enabled: true
+        mapped_regions: 
+          - id: region_1
+          - id: region_2
+          - id: region_3
+        providers: 
+          provider_1: 
+            types: [1, 2, 16, 32]
+          provider_2: 
+            types: [1, 2, 16, 32]
+          provider_3: 
+            types: [1, 2, 16, 32, 64]
+      - id: 4
+        topic: coriolis_worker
+        enabled: true
+        # missing region_1 or region_2
+        mapped_regions: 
+          - id: region_3
+        providers: 
+          provider_1: 
+            types: [1, 2, 16, 32]
+          provider_2: 
+            types: [1, 2, 16, 32]
+          provider_3: 
+            types: [1, 2, 16, 32, 64]
+      - id: 5
+        topic: coriolis_worker
+        enabled: true
+        mapped_regions: 
+          - id: region_1
+          - id: region_3
+        providers: 
+          provider_1: 
+            types: [1, 2, 16, 32]
+          provider_2: 
+            types: [1, 2, 16, 32]
+          provider_3: 
+            types: [1, 2, 16, 32, 64]
+      - id: 6
+        topic: coriolis_worker
+        # is not enabled
+        mapped_regions: 
+          - id: region_1
+          - id: region_3
+        providers: 
+          provider_1: 
+            types: [1, 2, 16, 32]
+          provider_2: 
+            types: [1, 2, 16, 32]
+          provider_3: 
+            types: [1, 2, 16, 32, 64]
+  expected_result: [1, 3, 5]
+  expected_exception: ~

+ 116 - 0
coriolis/tests/scheduler/rpc/test_server.py

@@ -0,0 +1,116 @@
+# Copyright 2023 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from unittest import mock
+
+import ddt
+
+from coriolis import exception
+from coriolis.scheduler.filters import trivial_filters
+from coriolis.scheduler.rpc import server
+from coriolis.tests import test_base
+from coriolis.tests import testutils
+
+
+@ddt.ddt
+class SchedulerServerEndpointTestCase(test_base.CoriolisBaseTestCase):
+    """Test suite for the Coriolis Scheduler Worker RPC server."""
+
+    def setUp(self):
+        super(SchedulerServerEndpointTestCase, self).setUp()
+        self.server = server.SchedulerServerEndpoint()
+
+    @mock.patch.object(trivial_filters, 'ProviderTypesFilter', autospec=True)
+    @mock.patch.object(trivial_filters, 'RegionsFilter', autospec=True)
+    @mock.patch.object(trivial_filters, 'EnabledFilter', autospec=True)
+    @mock.patch.object(
+        server.SchedulerServerEndpoint, '_get_weighted_filtered_services'
+    )
+    @mock.patch.object(
+        server.SchedulerServerEndpoint, '_filter_regions'
+    )
+    @mock.patch.object(
+        server.SchedulerServerEndpoint, '_get_all_worker_services'
+    )
+    @ddt.file_data("data/get_workers_for_specs_config.yaml")
+    @ddt.unpack
+    def test_get_workers_for_specs(
+            self,
+            mock_get_all_worker_services,
+            mock_filter_regions,
+            mock_get_weighted_filtered_services,
+            mock_enabled_filter_cls,
+            mock_regions_filter_cls,
+            mock_provider_types_filter_cls,
+            config,
+            expected_result,
+            expected_exception,
+    ):
+
+        enabled = config.get("enabled", None)
+        region_sets = config.get("region_sets", None)
+        provider_requirements = config.get("provider_requirements", None)
+
+        # Convert the config dict to an object, skipping the providers
+        # providers is the only field used as dict in the code
+        config_obj = testutils.DictToObject(config, skip_attrs=["providers"])
+        mock_get_all_worker_services.return_value = (
+            config_obj.services_db or []
+        )
+        mock_filter_regions.return_value = config_obj.regions_db or []
+        mock_get_weighted_filtered_services.return_value = \
+            [] if expected_result is None else [
+                (mock.Mock(id=expected_id), 100)
+                for expected_id in expected_result
+            ]
+
+        kwargs = {
+            "enabled": enabled,
+            "region_sets": region_sets,
+            "provider_requirements": provider_requirements,
+        }
+        if expected_exception:
+            exception_type = getattr(exception, expected_exception)
+            self.assertRaises(
+                exception_type,
+                self.server.get_workers_for_specs,
+                mock.sentinel.context,
+                **kwargs
+            )
+            return
+
+        result = self.server.get_workers_for_specs(
+            mock.sentinel.context,
+            **kwargs
+        )
+
+        mock_get_all_worker_services.assert_called_once_with(
+            mock.sentinel.context)
+
+        if region_sets:
+            calls = [mock.call(
+                mock.sentinel.context,
+                region_set,
+                enabled=True,
+                check_all_exist=True)
+                for region_set in region_sets]
+            mock_filter_regions.assert_has_calls(calls, any_order=True)
+
+        mock_get_weighted_filtered_services.assert_called_once_with(
+            mock_get_all_worker_services.return_value, mock.ANY
+        )
+
+        id_array = [worker.id for worker in result]
+
+        self.assertEqual(id_array, expected_result)
+
+        # Assertions for the trivial filter classes
+        if enabled is not None:
+            mock_enabled_filter_cls.assert_called_once_with(enabled=enabled)
+        if region_sets:
+            calls = [mock.call(region_set, any_region=True)
+                     for region_set in region_sets]
+            mock_regions_filter_cls.assert_has_calls(calls, any_order=True)
+        if provider_requirements:
+            mock_provider_types_filter_cls.assert_called_once_with(
+                provider_requirements)

+ 33 - 0
coriolis/tests/testutils.py

@@ -34,3 +34,36 @@ def get_wrapped_function(function):
         return function
 
     return _get_wrapped_function(function)
+
+
+class DictToObject:
+    """Converts a dictionary to an object with attributes.
+
+    This is useful for mocking objects that are used as configuration
+    objects.
+    """
+
+    def __init__(self, dictionary, skip_attrs=None):
+        if skip_attrs is None:
+            skip_attrs = []
+
+        for key, value in dictionary.items():
+            if key in skip_attrs:
+                setattr(self, key, value)
+            elif isinstance(value, dict):
+                setattr(self, key, DictToObject(value, skip_attrs=skip_attrs))
+            elif isinstance(value, list):
+                setattr(
+                    self, key,
+                    [DictToObject(item, skip_attrs=skip_attrs) if isinstance(
+                        item, dict) else item for item in value])
+            else:
+                setattr(self, key, value)
+
+    def __getattr__(self, item):
+        return None
+
+    def __repr__(self):
+        attrs = [f"{k}={v!r}" for k, v in self.__dict__.items()]
+        attrs_str = ', '.join(attrs)
+        return f"{self.__class__.__name__}({attrs_str})"