From 8b99a348398d85d721ddb482a60198b51c0fcd9c Mon Sep 17 00:00:00 2001 From: Sergiu Miclea Date: Mon, 1 May 2023 20:40:28 +0000 Subject: [PATCH] Add unit tests for Coriolis Scheduler RPC Server --- coriolis/tests/scheduler/__init__.py | 0 coriolis/tests/scheduler/rpc/__init__.py | 0 .../data/get_workers_for_specs_config.yaml | 224 ++++++++++++++++++ coriolis/tests/scheduler/rpc/test_server.py | 116 +++++++++ coriolis/tests/testutils.py | 33 +++ 5 files changed, 373 insertions(+) create mode 100644 coriolis/tests/scheduler/__init__.py create mode 100644 coriolis/tests/scheduler/rpc/__init__.py create mode 100644 coriolis/tests/scheduler/rpc/data/get_workers_for_specs_config.yaml create mode 100644 coriolis/tests/scheduler/rpc/test_server.py diff --git a/coriolis/tests/scheduler/__init__.py b/coriolis/tests/scheduler/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/coriolis/tests/scheduler/rpc/__init__.py b/coriolis/tests/scheduler/rpc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/coriolis/tests/scheduler/rpc/data/get_workers_for_specs_config.yaml b/coriolis/tests/scheduler/rpc/data/get_workers_for_specs_config.yaml new file mode 100644 index 000000000..bd809484f --- /dev/null +++ b/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: ~ diff --git a/coriolis/tests/scheduler/rpc/test_server.py b/coriolis/tests/scheduler/rpc/test_server.py new file mode 100644 index 000000000..c02871384 --- /dev/null +++ b/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) diff --git a/coriolis/tests/testutils.py b/coriolis/tests/testutils.py index f98d914df..c822e3a07 100644 --- a/coriolis/tests/testutils.py +++ b/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})"