diff options
author | 2018-03-07 22:21:40 +0800 | |
---|---|---|
committer | 2018-03-07 10:44:17 -0800 | |
commit | 6ac2dff39d6edb6c411a641036f474bb2e493f5b (patch) | |
tree | 3f6d9a02c4fb9138ca3cc4d1d24c597432e45013 | |
parent | da17dd41a33cf488d16a2adedbca5c81cb3f75fc (diff) |
Modified unit tests for Controller Module
Unit test for translator.py, rpc.py and translator_svc.py in conductor/controller
Change-Id: I382078152d7708b6946b68902151529dcf1f3be4
Issue-ID: OPTFRA-69
Signed-off-by: Ying Ruoyu <ruoyu.ying@intel.com>
5 files changed, 560 insertions, 0 deletions
diff --git a/conductor/conductor/tests/data/some_template.yaml b/conductor/conductor/tests/data/some_template.yaml new file mode 100644 index 0000000..f37212f --- /dev/null +++ b/conductor/conductor/tests/data/some_template.yaml @@ -0,0 +1,81 @@ +#Homing Specification Version +homing_template_version: 2017-10-10 + +# Runtime order Parameters +parameters: + service_name: Residential vCPE + service_id: vcpe_service_id + customer_lat: 32.897480 + customer_long: -97.040443 + +# List of geographical locations +locations: + customer_loc: + latitude: {get_param: customer_lat} + longitude: {get_param: customer_long} + +# List of VNFs (demands) to be homed +demands: + vGMuxInfra: + - inventory_provider: aai + inventory_type: service + attributes: + equipment_type: vG_Mux + customer_id: some_company + excluded_candidates: + - candidate_id: 1ac71fb8-ad43-4e16-9459-c3f372b8236d + existing_placement: + - candidate_id: 21d5f3e8-e714-4383-8f99-cc480144505a + vG: + - inventory_provider: aai + inventory_type: service + attributes: + equipment_type: vG + modelId: vG_model_id + customer_id: some_company + excluded_candidates: + - candidate_id: 1ac71fb8-ad43-4e16-9459-c3f372b8236d + existing_placement: + - candidate_id: 21d5f3e8-e714-4383-8f99-cc480144505a + - inventory_provider: aai + inventory_type: cloud + +# List of homing policies (constraints) +constraints: + # distance constraint + - constraint_vgmux_customer: + type: distance_to_location + demands: [vGMuxInfra] + properties: + distance: < 100 km + location: customer_loc + # cloud region co-location constraint + - colocation: + type: zone + demands: [vGMuxInfra, vG] + properties: + qualifier: same + category: region + # platform capability constraint + - numa_cpu_pin_capabilities: + type: attribute + demands: [vG] + properties: + evaluate: + vcpu_pinning: True + numa_topology: numa_spanning + # cloud provider constraint + - cloud_version_capabilities: + type: attribute + demands: [vGMuxInfra] + properties: + evaluate: + cloud_version: 1.11.84 + cloud_provider: AWS + +# Objective function to minimize +optimization: + minimize: + sum: + - {distance_between: [customer_loc, vGMuxInfra]} + - {distance_between: [customer_loc, vG]} diff --git a/conductor/conductor/tests/unit/controller/__init__.py b/conductor/conductor/tests/unit/controller/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/conductor/conductor/tests/unit/controller/__init__.py diff --git a/conductor/conductor/tests/unit/controller/test_rpc.py b/conductor/conductor/tests/unit/controller/test_rpc.py new file mode 100644 index 0000000..1039468 --- /dev/null +++ b/conductor/conductor/tests/unit/controller/test_rpc.py @@ -0,0 +1,120 @@ +# +# ------------------------------------------------------------------------ +# Copyright (c) 2018 Intel Corporation Intellectual Property +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------- +# +"""Test classes for rpc""" + +import unittest +import uuid + +from conductor.controller.rpc import ControllerRPCEndpoint as rpc +from conductor import service +from conductor.common.models import plan +from conductor.common.music.model import base +from conductor.common.music import api +from oslo_config import cfg +from mock import patch + + +def plan_prepare(conf): + music = api.API() + music.keyspace_create(keyspace=conf.keyspace) + plan_tmp = base.create_dynamic_model( + keyspace=conf.keyspace, baseclass=plan.Plan, classname="Plan") + return plan_tmp + + +class TestRPCNoException(unittest.TestCase): + def setUp(self): + cfg.CONF.set_override('timeout', 10, 'controller') + cfg.CONF.set_override('limit', 1, 'controller') + cfg.CONF.set_override('keyspace', 'conductor') + cfg.CONF.set_override('mock', True, 'music_api') + conf = cfg.CONF + plan_class = plan_prepare(conf) + self.r = rpc(conf, plan_class) + self._cvx = "" + self._arg = { + "name": str(uuid.uuid4()), + "timeout": conf, + "limit": conf, + "template": None + } + self.plan_expected = { + "plan": { + "name": "null", + "id": "null", + "status": "null" + } + } + self.plan_mock = [] + element = plan_prepare(conf) + setattr(element, "name", "null") + setattr(element, "id", "null") + setattr(element, "status", "null") + setattr(element, "message", "null") + e = {'recommendations': 'null'} + setattr(element, "solution", e) + self.plan_mock.append(element) + self.the_plan_expected = [{ + "name": "null", + "id": "null", + "status": "null", + "message": "null", + "recommendations": "null" + }] + + def test_plan_creation(self): + a_arg = [] + b_arg = [] + rtn = self.r.plan_create(self._cvx, self._arg) + for k in sorted(rtn.get('response')): + a_arg.append(k) + for key in sorted(self.plan_expected): + b_arg.append(key) + self.assertEquals(rtn.get('error'), False) + self.assertEquals(a_arg, b_arg) + for k in sorted(rtn.get('response').get('plan')): + a_arg.append(k) + for key in sorted(self.plan_expected.get('plan')): + b_arg.append(key) + self.assertEquals(a_arg, b_arg) + + @patch('conductor.common.music.model.search.Query.all') + def test_plan_get_same_schema(self, mock_query): + _id = {} + mock_query.return_value = self.plan_mock + rtn_get = self.r.plans_get(self._cvx, _id) + plans = rtn_get.get('response').get('plans') + self.assertEquals(plans, self.the_plan_expected) + self.assertFalse(rtn_get.get('error')) + + @patch('conductor.common.music.model.search.Query.all') + @patch('conductor.common.music.model.base.Base.delete') + def test_plans_delete(self, mock_delete, mock_call): + _id = {} + mock_call.return_value = self.plan_mock + rtn = self.r.plans_delete(self._cvx, _id) + self.assertEquals(rtn.get('response'), {}) + self.assertFalse(rtn.get('error')) + + def tearDown(self): + patch.stopall() + + +if __name__ == '__main__': + unittest.main() diff --git a/conductor/conductor/tests/unit/controller/test_translator.py b/conductor/conductor/tests/unit/controller/test_translator.py new file mode 100644 index 0000000..2dbee00 --- /dev/null +++ b/conductor/conductor/tests/unit/controller/test_translator.py @@ -0,0 +1,268 @@ +# +# ------------------------------------------------------------------------ +# Copyright (c) 2018 Intel Corporation Intellectual Property +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------- +# +"""Test classes for translator""" + +import os +import yaml +import uuid +import unittest + +from conductor.controller.translator import Translator +from conductor.controller.translator import TranslatorException +from conductor import __file__ as conductor_root +from oslo_config import cfg +from mock import patch + + +def get_template(): + template_name = 'some_template' + path = os.path.abspath(conductor_root) + dir_path = os.path.dirname(path) + template_file = dir_path + '/tests/data/' + template_name + '.yaml' + fd = open(template_file, "r") + template = yaml.load(fd) + return template + + +class TestNoExceptionTranslator(unittest.TestCase): + @patch('conductor.common.music.model.base.Base.table_create') + def setUp(self, mock_table_create): + cfg.CONF.set_override('keyspace', 'conductor') + cfg.CONF.set_override('keyspace', 'conductor_rpc', 'messaging_server') + cfg.CONF.set_override('concurrent', True, 'controller') + cfg.CONF.set_override('mock', True, 'music_api') + conf = cfg.CONF + self.Translator = Translator( + conf, 'some_template', str(uuid.uuid4()), get_template()) + + def test_create_correct_components(self): + self.Translator.create_components() + self.assertIsNotNone(self.Translator._version) + self.assertIsNotNone(self.Translator._parameters) + self.assertIsNotNone(self.Translator._constraints) + self.assertIsNotNone(self.Translator._demands) + self.assertIsNotNone(self.Translator._locations) + self.assertIsNotNone(self.Translator._reservations) + self.assertIsNotNone(self.Translator._optmization) + self.assertIsInstance(self.Translator._version, str) + + def test_error_version_validation(self): + self.Translator._version = "2016-11-02" + self.assertRaises(TranslatorException, + self.Translator.validate_components, ) + + def test_error_format_validation(self): + self.Translator._version = "2016-11-01" + self.Translator._locations = "" + self.Translator._demands = "" + self.Translator._constraints = "" + self.Translator._reservations = "" + self.Translator._optmization = "" + self.Translator._parameters = "" + self.assertRaises(TranslatorException, + self.Translator.validate_components, ) + + def test_validation_complete(self): + self.Translator._demands = {'vG': ''} + self.Translator._version = "2016-11-01" + self.Translator._locations = {'custom_loc': {'longitude': ''}} + self.Translator._constraints = {'vG': { + 'demands': 'vG', + 'properties': {'location': 'custom_loc'}}} + self.Translator._parameters = {} + self.Translator._optmization = {} + self.Translator._reservations = {} + self.Translator.validate_components() + self.assertTrue(self.Translator._valid) + + def test_parse_parameter(self): + self.Translator.create_components() + rtn = self.Translator._parse_parameters( + self.Translator._locations, "locations") + location = {'customer_loc': { + 'latitude': 32.89748, 'longitude': -97.040443}} + self.assertEquals(rtn, location) + + @patch('conductor.common.music.messaging.component.RPCClient.call') + def test_parse_locations(self, mock_call): + locations = {'customer_loc': { + 'latitude': 32.89748, 'longitude': -97.040443} + } + mock_call.return_value = {'resolved_location': { + 'latitude': 32.89748, 'longitude': -97.040443}} + self.assertEquals( + self.Translator.parse_locations(locations), locations) + + def test_parse_error_format_demands(self): + demands = "" + self.assertRaises(TranslatorException, + self.Translator.parse_demands, demands) + + @patch('conductor.common.music.messaging.component.RPCClient.call') + def test_parse_demands_without_candidate(self, mock_call): + demands = { + "vGMuxInfra": [{ + "inventory_provider": "aai", + "inventory_type": "service", + "customer_id": "some_company", + "service_type": "5G", + "excluded_candidates": [{ + "candidate_id": "1ac71fb8-ad43-4e16-9459-c3f372b8236d" + }], + "required_candidates": [{ + "candidate_id": "1a9983b8-0o43-4e16-9947-c3f37234536d" + }] + } + ]} + self.Translator._plan_id = "" + self.Translator._plan_name = "" + mock_call.return_value = {'resolved_demands': {"vGMuxInfra": [{ + "inventory_provider": "aai", + "inventory_type": "service", + "customer_id": "some_company", + "service_type": "5G", + "excluded_candidates": [{ + "candidate_id:1ac71fb8-ad43-4e16-9459-c3f372b8236d" + }], + "required_candidates": [{ + "candidate_id": "1a9983b8-0o43-4e16-9947-c3f37234536d"}] + + }] + }} + rtn = {'vGMuxInfra': {'candidates': [{ + 'customer_id': 'some_company', + 'excluded_candidates': [set([ + 'candidate_id:1ac71fb8-ad43-4e16-9459-c3f372b8236d'])], + 'inventory_provider': 'aai', + 'inventory_type': 'service', + 'required_candidates': [{ + 'candidate_id': '1a9983b8-0o43-4e16-9947-c3f37234536d' + }], + 'service_type': '5G'}]}} + + self.assertEquals(self.Translator.parse_demands(demands), rtn) + + def test_parse_constraints(self): + constraints = {'constraint_loc': { + 'type': 'distance_to_location', + 'demands': ['vG'], + 'properties': {'distance': '< 100 km', + 'location': 'custom_loc'}}} + rtn = {'constraint_loc_vG': { + 'demands': 'vG', + 'name': 'constraint_loc', + 'properties': {'distance': {'operator': '<', + 'units': 'km', + 'value': 100.0}, + 'location': 'custom_loc'}, + 'type': 'distance_to_location'}} + self.assertEquals(self.Translator.parse_constraints(constraints), rtn) + + # TODO(ruoyu) + @patch('conductor.controller.translator.Translator.create_components') + def parse_optimization(self, mock_create): + args = ['customer_loc', 'vGMuxInfra'] + func = 'distance_between' + expected_parse = { + "goal": "min", + "operation": "sum", + "operands": [{"operation": "product", + "weight": 1.0, + "function": func, + "function_param": args}] + } + opt = {'minimize': { + 'sum': [{ + 'distance_between': ['customer_loc', 'vGMuxInfra']}, { + 'distance_between': ['customer_loc', 'vG']}]}} + self.Translator._demands = {'vG': '', + 'vGMuxInfra': '', + 'customer_loc': ''} + self.Translator._locations = {'vG': '', + 'vGMuxInfra': '', + 'customer_loc': ''} + self.assertEquals( + self.Translator.parse_optimization( + opt), expected_parse) + + @patch('conductor.controller.translator.Translator.create_components') + def test_parse_reservation(self, mock_create): + expected_resv = {'counter': 0, 'demands': { + 'instance_vG': {'demands': {'vG': 'null'}, + 'properties': {}, + 'name': 'instance', + 'demand': 'vG'}}} + self.Translator._demands = {'vG': 'null'} + resv = { + 'instance': {'demands': {'vG': 'null'}} + } + self.assertEquals( + self.Translator.parse_reservations(resv), expected_resv) + + @patch('conductor.controller.translator.Translator.parse_constraints') + @patch('conductor.controller.translator.Translator.parse_reservations') + @patch('conductor.controller.translator.Translator.parse_demands') + @patch('conductor.controller.translator.Translator.parse_optimization') + @patch('conductor.controller.translator.Translator.parse_locations') + def test_do_translation(self, mock_loc, mock_opt, + mock_dmd, mock_resv, mock_cons): + expected_format = { + "conductor_solver": { + "version": '', + "plan_id": '', + "locations": {}, + "request_type": '', + "demands": {}, + "constraints": {}, + "objective": {}, + "reservations": {}, + } + } + self.Translator._valid = True + self.Translator._version = '' + self.Translator._plan_id = '' + self.Translator._parameters = {} + self.Translator._locations = {} + self.Translator._demands = {} + self.Translator._constraints = {} + self.Translator._optmization = {} + self.Translator._reservations = {} + mock_loc.return_value = {} + mock_resv.return_value = {} + mock_dmd.return_value = {} + mock_opt.return_value = {} + mock_cons.return_value = {} + self.Translator.do_translation() + self.assertEquals(self.Translator._translation, expected_format) + + @patch('conductor.controller.translator.Translator.create_components') + @patch('conductor.controller.translator.Translator.validate_components') + @patch('conductor.controller.translator.Translator.do_translation') + @patch('conductor.controller.translator.Translator.parse_parameters') + def test_translate(self, mock_parse, mock_do_trans, + mock_valid, mock_create): + self.Translator.translate() + self.assertEquals(self.Translator._ok, True) + + def tearDown(self): + patch.stopall() + + +if __name__ == '__main__': + unittest.main() diff --git a/conductor/conductor/tests/unit/controller/test_translator_svc.py b/conductor/conductor/tests/unit/controller/test_translator_svc.py new file mode 100644 index 0000000..051e9d3 --- /dev/null +++ b/conductor/conductor/tests/unit/controller/test_translator_svc.py @@ -0,0 +1,91 @@ +# +# ------------------------------------------------------------------------ +# Copyright (c) 2018 Intel Corporation Intellectual Property +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------- +# +"""Test classes for translator_svc""" + +import unittest +import uuid + +from mock import patch +from mock import PropertyMock +from conductor.controller.translator_svc import TranslatorService +from conductor.common.models import plan +from conductor.common.music import api +from conductor.common.music.model import base +from oslo_config import cfg + + +def plan_prepare(conf): + music = api.API() + music.keyspace_create(keyspace=conf.keyspace) + plan_tmp = base.create_dynamic_model( + keyspace=conf.keyspace, baseclass=plan.Plan, classname="Plan") + return plan_tmp + + +class TestTranslatorServiceNoException(unittest.TestCase): + def setUp(self): + cfg.CONF.set_override('polling_interval', 1, 'controller') + cfg.CONF.set_override('keyspace', 'conductor') + cfg.CONF.set_override('timeout', 10, 'controller') + cfg.CONF.set_override('limit', 1, 'controller') + cfg.CONF.set_override('concurrent', True, 'controller') + cfg.CONF.set_override('keyspace', + 'conductor_rpc', 'messaging_server') + cfg.CONF.set_override('mock', True, 'music_api') + self.conf = cfg.CONF + self.Plan = plan_prepare(self.conf) + kwargs = self.Plan + name = str(uuid.uuid4()) + timeout = self.conf.controller.timeout + recommend_max = self.conf.controller.limit + template = None + status = self.Plan.TEMPLATE + self.mock_plan = self.Plan(name, timeout, recommend_max, template, + status=status) + self.translator_svc = TranslatorService( + worker_id=1, conf=self.conf, plan_class=kwargs) + self.translator_svc.music.keyspace_create(keyspace=self.conf.keyspace) + + #TODO(ruoyu) + @patch('conductor.controller.translator.Translator.ok') + def translate_complete(self, mock_ok_func): + with patch('conductor.controller.translator.Translator.ok', + new_callable=PropertyMock) as mock_ok: + mock_ok.return_value = True + mock_ok_func.return_value = True + self.translator_svc.translate(self.mock_plan) + self.assertEquals(self.mock_plan.status, 'translated') + + # TODO(ruoyu) + @patch('conductor.controller.translator.Translator.translate') + @patch('conductor.controller.translator.Translator.error_message') + def translate_error(self, mock_error, mock_trns): + with patch('conductor.controller.translator.Translator.ok', + new_callable=PropertyMock) as mock_ok: + mock_ok.return_value = False + mock_error.return_value = 'error' + self.translator_svc.translate(self.mock_plan) + self.assertEquals(self.mock_plan.status, 'error') + + def tearDown(self): + patch.stopall() + + +if __name__ == '__main__': + unittest.main() |