Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unit tests for Coriolis Scheduler RPC Server #257

Merged
merged 1 commit into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
Empty file.
224 changes: 224 additions & 0 deletions coriolis/tests/scheduler/rpc/data/get_workers_for_specs_config.yaml
Original file line number Diff line number Diff line change
@@ -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 changes: 116 additions & 0 deletions coriolis/tests/scheduler/rpc/test_server.py
Original file line number Diff line number Diff line change
@@ -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
smiclea marked this conversation as resolved.
Show resolved Hide resolved
)

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 changes: 33 additions & 0 deletions coriolis/tests/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})"