Modified unit tests for Controller Module 29/34429/2
authorYing Ruoyu <ruoyu.ying@intel.com>
Wed, 7 Mar 2018 14:21:40 +0000 (22:21 +0800)
committerDileep Ranganathan <dileep.ranganathan@intel.com>
Wed, 7 Mar 2018 18:44:17 +0000 (10:44 -0800)
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>
conductor/conductor/tests/data/some_template.yaml [new file with mode: 0644]
conductor/conductor/tests/unit/controller/__init__.py [new file with mode: 0644]
conductor/conductor/tests/unit/controller/test_rpc.py [new file with mode: 0644]
conductor/conductor/tests/unit/controller/test_translator.py [new file with mode: 0644]
conductor/conductor/tests/unit/controller/test_translator_svc.py [new file with mode: 0644]

diff --git a/conductor/conductor/tests/data/some_template.yaml b/conductor/conductor/tests/data/some_template.yaml
new file mode 100644 (file)
index 0000000..f37212f
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/conductor/conductor/tests/unit/controller/test_rpc.py b/conductor/conductor/tests/unit/controller/test_rpc.py
new file mode 100644 (file)
index 0000000..1039468
--- /dev/null
@@ -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 (file)
index 0000000..2dbee00
--- /dev/null
@@ -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 (file)
index 0000000..051e9d3
--- /dev/null
@@ -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()