From f665d836576b15bb9ed807dc963cecb6734f1697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Fran=C3=A7oise?= Date: Wed, 18 May 2016 15:55:04 +0200 Subject: [PATCH] Added efficacy specification to /goals In this changeset, I added the new Efficacy, EfficacySpecification and IndicatorSpecification classes which are the main components for computing an efficacy. Partially Implements: blueprint efficacy-indicator Change-Id: I3a1d62569de2dd6bb6f9a52f6058313fa2b886ce --- watcher/api/controllers/v1/goal.py | 35 +++-- watcher/common/exception.py | 5 + watcher/db/sqlalchemy/models.py | 1 + watcher/decision_engine/goal/base.py | 6 + .../decision_engine/goal/efficacy/__init__.py | 0 watcher/decision_engine/goal/efficacy/base.py | 75 ++++++++++ .../goal/efficacy/indicators.py | 132 ++++++++++++++++++ .../decision_engine/goal/efficacy/specs.py | 52 +++++++ watcher/decision_engine/goal/goals.py | 38 +++++ watcher/decision_engine/solution/efficacy.py | 27 ++++ watcher/decision_engine/sync.py | 23 ++- watcher/objects/goal.py | 1 + watcher/tests/api/v1/test_goals.py | 3 +- watcher/tests/db/utils.py | 1 + watcher/tests/decision_engine/fake_goals.py | 38 +++++ watcher/tests/decision_engine/test_sync.py | 35 ++++- 16 files changed, 450 insertions(+), 22 deletions(-) create mode 100644 watcher/decision_engine/goal/efficacy/__init__.py create mode 100644 watcher/decision_engine/goal/efficacy/base.py create mode 100644 watcher/decision_engine/goal/efficacy/indicators.py create mode 100644 watcher/decision_engine/goal/efficacy/specs.py create mode 100644 watcher/decision_engine/solution/efficacy.py diff --git a/watcher/api/controllers/v1/goal.py b/watcher/api/controllers/v1/goal.py index b50ae0513..06f284595 100644 --- a/watcher/api/controllers/v1/goal.py +++ b/watcher/api/controllers/v1/goal.py @@ -68,24 +68,28 @@ class Goal(base.APIBase): display_name = wtypes.text """Localized name of the goal""" + efficacy_specification = wtypes.wsattr(types.jsontype, readonly=True) + """Efficacy specification for this goal""" + links = wsme.wsattr([link.Link], readonly=True) """A list containing a self link and associated audit template links""" def __init__(self, **kwargs): - super(Goal, self).__init__() - self.fields = [] - self.fields.append('uuid') - self.fields.append('name') - self.fields.append('display_name') - setattr(self, 'uuid', kwargs.get('uuid', wtypes.Unset)) - setattr(self, 'name', kwargs.get('name', wtypes.Unset)) - setattr(self, 'display_name', kwargs.get('display_name', wtypes.Unset)) + fields = list(objects.Goal.fields) + + for k in fields: + # Skip fields we do not expose. + if not hasattr(self, k): + continue + self.fields.append(k) + setattr(self, k, kwargs.get(k, wtypes.Unset)) @staticmethod def _convert_with_links(goal, url, expand=True): if not expand: - goal.unset_fields_except(['uuid', 'name', 'display_name']) + goal.unset_fields_except(['uuid', 'name', 'display_name', + 'efficacy_specification']) goal.links = [link.Link.make_link('self', url, 'goals', goal.uuid), @@ -101,9 +105,16 @@ class Goal(base.APIBase): @classmethod def sample(cls, expand=True): - sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c', - name='DUMMY', - display_name='Dummy strategy') + sample = cls( + uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c', + name='DUMMY', + display_name='Dummy strategy', + efficacy_specification=[ + {'description': 'Dummy indicator', 'name': 'dummy', + 'schema': 'Range(min=0, max=100, min_included=True, ' + 'max_included=True, msg=None)', + 'unit': '%'} + ]) return cls._convert_with_links(sample, 'http://localhost:9322', expand) diff --git a/watcher/common/exception.py b/watcher/common/exception.py index 63cdc598c..7fb131eab 100644 --- a/watcher/common/exception.py +++ b/watcher/common/exception.py @@ -306,6 +306,11 @@ class NoAvailableStrategyForGoal(WatcherException): msg_fmt = _("No strategy could be found to achieve the '%(goal)s' goal.") +class InvalidIndicatorValue(WatcherException): + msg_fmt = _("The indicator '%(name)s' with value '%(value)s' " + "and spec type '%(spec_type)s' is invalid.") + + class NoMetricValuesForVM(WatcherException): msg_fmt = _("No values returned by %(resource_id)s for %(metric_name)s.") diff --git a/watcher/db/sqlalchemy/models.py b/watcher/db/sqlalchemy/models.py index f97af8ab9..245942dda 100644 --- a/watcher/db/sqlalchemy/models.py +++ b/watcher/db/sqlalchemy/models.py @@ -138,6 +138,7 @@ class Goal(Base): uuid = Column(String(36)) name = Column(String(63), nullable=False) display_name = Column(String(63), nullable=False) + efficacy_specification = Column(JSONEncodedList, nullable=False) class AuditTemplate(Base): diff --git a/watcher/decision_engine/goal/base.py b/watcher/decision_engine/goal/base.py index 54c5a672a..7f5d32585 100644 --- a/watcher/decision_engine/goal/base.py +++ b/watcher/decision_engine/goal/base.py @@ -31,6 +31,7 @@ class Goal(loadable.Loadable): super(Goal, self).__init__(config) self.name = self.get_name() self.display_name = self.get_display_name() + self.efficacy_specification = self.get_efficacy_specification() @classmethod @abc.abstractmethod @@ -60,3 +61,8 @@ class Goal(loadable.Loadable): :rtype: list of :class:`oslo_config.cfg.Opt` instances """ return [] + + @abc.abstractmethod + def get_efficacy_specification(cls): + """The efficacy spec for the current goal""" + raise NotImplementedError() diff --git a/watcher/decision_engine/goal/efficacy/__init__.py b/watcher/decision_engine/goal/efficacy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/watcher/decision_engine/goal/efficacy/base.py b/watcher/decision_engine/goal/efficacy/base.py new file mode 100644 index 000000000..cff52bf85 --- /dev/null +++ b/watcher/decision_engine/goal/efficacy/base.py @@ -0,0 +1,75 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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. + +import abc +import json + +import six +import voluptuous + + +@six.add_metaclass(abc.ABCMeta) +class EfficacySpecification(object): + + def __init__(self): + self._indicators_specs = self.get_indicators_specifications() + + @property + def indicators_specs(self): + return self._indicators_specs + + @abc.abstractmethod + def get_indicators_specifications(self): + """List the specifications of the indicator for this efficacy spec + + :return: Tuple of indicator specifications + :rtype: Tuple of :py:class:`~.IndicatorSpecification` instances + """ + raise NotImplementedError() + + @abc.abstractmethod + def get_global_efficacy_indicator(self, indicators_map): + """Compute the global efficacy for the goal it achieves + + :param indicators_map: dict-like object containing the + efficacy indicators related to this spec + :type indicators_map: :py:class:`~.IndicatorsMap` instance + :raises: NotImplementedError + :returns: :py:class:`~.Indicator` instance + """ + raise NotImplementedError() + + @property + def schema(self): + """Combined schema from the schema of the indicators""" + schema = voluptuous.Schema({}, required=True) + for indicator in self.indicators_specs: + key_constraint = (voluptuous.Required + if indicator.required else voluptuous.Optional) + schema = schema.extend( + {key_constraint(indicator.name): indicator.schema.schema}) + + return schema + + def validate_efficacy_indicators(self, indicators_map): + return self.schema(indicators_map) + + def get_indicators_specs_dicts(self): + return [indicator.to_dict() + for indicator in self.indicators_specs] + + def serialize_indicators_specs(self): + return json.dumps(self.get_indicators_specs_dicts()) diff --git a/watcher/decision_engine/goal/efficacy/indicators.py b/watcher/decision_engine/goal/efficacy/indicators.py new file mode 100644 index 000000000..719b3c576 --- /dev/null +++ b/watcher/decision_engine/goal/efficacy/indicators.py @@ -0,0 +1,132 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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. + +import abc +import six + +from oslo_log import log +import voluptuous + +from watcher._i18n import _ +from watcher.common import exception + +LOG = log.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class IndicatorSpecification(object): + + def __init__(self, name=None, description=None, unit=None, required=True): + self.name = name + self.description = description + self.unit = unit + self.required = required + + @abc.abstractproperty + def schema(self): + """Schema used to validate the indicator value + + :return: A Voplutuous Schema + :rtype: :py:class:`.voluptuous.Schema` instance + """ + raise NotImplementedError() + + @classmethod + def validate(cls, solution): + """Validate the given solution + + :raises: :py:class:`~.InvalidIndicatorValue` when the validation fails + """ + indicator = cls() + value = None + try: + value = getattr(solution, indicator.name) + indicator.schema(value) + except Exception as exc: + LOG.exception(exc) + raise exception.InvalidIndicatorValue( + name=indicator.name, value=value, spec_type=type(indicator)) + + def to_dict(self): + return { + "name": self.name, + "description": self.description, + "unit": self.unit, + "schema": str(self.schema.schema) if self.schema else None, + } + + def __str__(self): + return str(self.to_dict()) + + +class AverageCpuLoad(IndicatorSpecification): + + def __init__(self): + super(AverageCpuLoad, self).__init__( + name="avg_cpu_percent", + description=_("Average CPU load as a percentage of the CPU time."), + unit="%", + ) + + @property + def schema(self): + return voluptuous.Schema( + voluptuous.Range(min=0, max=100), required=True) + + +class MigrationEfficacy(IndicatorSpecification): + + def __init__(self): + super(MigrationEfficacy, self).__init__( + name="migration_efficacy", + description=_("Represents the percentage of released nodes out of " + "the total number of migrations."), + unit="%", + required=True + ) + + @property + def schema(self): + return voluptuous.Schema( + voluptuous.Range(min=0, max=100), required=True) + + +class ReleasedComputeNodesCount(IndicatorSpecification): + def __init__(self): + super(ReleasedComputeNodesCount, self).__init__( + name="released_compute_nodes_count", + description=_("The number of compute nodes to be released."), + unit=None, + ) + + @property + def schema(self): + return voluptuous.Schema( + voluptuous.Range(min=0), required=True) + + +class VmMigrationsCount(IndicatorSpecification): + def __init__(self): + super(VmMigrationsCount, self).__init__( + name="vm_migrations_count", + description=_("The number of migrations to be performed."), + unit=None, + ) + + @property + def schema(self): + return voluptuous.Schema( + voluptuous.Range(min=0), required=True) diff --git a/watcher/decision_engine/goal/efficacy/specs.py b/watcher/decision_engine/goal/efficacy/specs.py new file mode 100644 index 000000000..48e7d56f0 --- /dev/null +++ b/watcher/decision_engine/goal/efficacy/specs.py @@ -0,0 +1,52 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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. + +from watcher._i18n import _ +from watcher.decision_engine.goal.efficacy import base +from watcher.decision_engine.goal.efficacy import indicators +from watcher.decision_engine.solution import efficacy + + +class Unclassified(base.EfficacySpecification): + + def get_indicators_specifications(self): + return () + + def get_global_efficacy_indicator(self, indicators_map): + return None + + +class ServerConsolidation(base.EfficacySpecification): + + def get_indicators_specifications(self): + return [ + indicators.ReleasedComputeNodesCount(), + indicators.VmMigrationsCount(), + ] + + def get_global_efficacy_indicator(self, indicators_map): + value = 0 + if indicators_map.vm_migrations_count > 0: + value = (float(indicators_map.released_compute_nodes_count) / + float(indicators_map.vm_migrations_count)) * 100 + + return efficacy.Indicator( + name="released_nodes_ratio", + description=_("Ratio of released compute nodes divided by the " + "number of VM migrations."), + unit='%', + value=value, + ) diff --git a/watcher/decision_engine/goal/goals.py b/watcher/decision_engine/goal/goals.py index 8ea0bb573..ca26944cb 100644 --- a/watcher/decision_engine/goal/goals.py +++ b/watcher/decision_engine/goal/goals.py @@ -16,9 +16,14 @@ from watcher._i18n import _ from watcher.decision_engine.goal import base +from watcher.decision_engine.goal.efficacy import specs class Dummy(base.Goal): + """Dummy + + Reserved goal that is used for testing purposes. + """ @classmethod def get_name(cls): @@ -32,8 +37,21 @@ class Dummy(base.Goal): def get_translatable_display_name(cls): return "Dummy goal" + @classmethod + def get_efficacy_specification(cls): + """The efficacy spec for the current goal""" + return specs.Unclassified() + class Unclassified(base.Goal): + """Unclassified + + This goal is used to ease the development process of a strategy. Containing + no actual indicator specification, this goal can be used whenever a + strategy has yet to be formally associated with an existing goal. If the + goal achieve has been identified but there is no available implementation, + this Goal can also be used as a transitional stage. + """ @classmethod def get_name(cls): @@ -47,6 +65,11 @@ class Unclassified(base.Goal): def get_translatable_display_name(cls): return "Unclassified" + @classmethod + def get_efficacy_specification(cls): + """The efficacy spec for the current goal""" + return specs.Unclassified() + class ServerConsolidation(base.Goal): @@ -62,6 +85,11 @@ class ServerConsolidation(base.Goal): def get_translatable_display_name(cls): return "Server consolidation" + @classmethod + def get_efficacy_specification(cls): + """The efficacy spec for the current goal""" + return specs.ServerConsolidation() + class ThermalOptimization(base.Goal): @@ -77,6 +105,11 @@ class ThermalOptimization(base.Goal): def get_translatable_display_name(cls): return "Thermal optimization" + @classmethod + def get_efficacy_specification(cls): + """The efficacy spec for the current goal""" + return specs.Unclassified() + class WorkloadBalancing(base.Goal): @@ -91,3 +124,8 @@ class WorkloadBalancing(base.Goal): @classmethod def get_translatable_display_name(cls): return "Workload balancing" + + @classmethod + def get_efficacy_specification(cls): + """The efficacy spec for the current goal""" + return specs.Unclassified() diff --git a/watcher/decision_engine/solution/efficacy.py b/watcher/decision_engine/solution/efficacy.py new file mode 100644 index 000000000..827a0d87d --- /dev/null +++ b/watcher/decision_engine/solution/efficacy.py @@ -0,0 +1,27 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# 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. + +from watcher.common import utils + + +class Indicator(utils.Struct): + + def __init__(self, name, description, unit, value): + super(Indicator, self).__init__() + self.name = name + self.description = description + self.unit = unit + self.value = value diff --git a/watcher/decision_engine/sync.py b/watcher/decision_engine/sync.py index 9ee6d92c2..092baf06d 100644 --- a/watcher/decision_engine/sync.py +++ b/watcher/decision_engine/sync.py @@ -26,7 +26,7 @@ from watcher import objects LOG = log.getLogger(__name__) GoalMapping = collections.namedtuple( - 'GoalMapping', ['name', 'display_name']) + 'GoalMapping', ['name', 'display_name', 'efficacy_specification']) StrategyMapping = collections.namedtuple( 'StrategyMapping', ['name', 'goal_name', 'display_name']) @@ -75,7 +75,10 @@ class Syncer(object): self._available_goals_map = { GoalMapping( name=g.name, - display_name=g.display_name): g + display_name=g.display_name, + efficacy_specification=tuple( + IndicatorSpec(**item) + for item in g.efficacy_specification)): g for g in self.available_goals } return self._available_goals_map @@ -127,6 +130,9 @@ class Syncer(object): goal = objects.Goal(self.ctx) goal.name = goal_name goal.display_name = goal_map.display_name + goal.efficacy_specification = [ + indicator._asdict() + for indicator in goal_map.efficacy_specification] goal.create() LOG.info(_LI("Goal %s created"), goal_name) @@ -140,6 +146,7 @@ class Syncer(object): def _sync_strategy(self, strategy_map): strategy_name = strategy_map.name + strategy_display_name = strategy_map.display_name goal_name = strategy_map.goal_name strategy_mapping = dict() @@ -153,7 +160,7 @@ class Syncer(object): if stale_strategies or not matching_strategies: strategy = objects.Strategy(self.ctx) strategy.name = strategy_name - strategy.display_name = strategy_map.display_name + strategy.display_name = strategy_display_name strategy.goal_id = objects.Goal.get_by_name(self.ctx, goal_name).id strategy.create() LOG.info(_LI("Strategy %s created"), strategy_name) @@ -267,7 +274,11 @@ class Syncer(object): for _, goal_cls in implemented_goals.items(): goals_map[goal_cls.get_name()] = GoalMapping( name=goal_cls.get_name(), - display_name=goal_cls.get_translatable_display_name()) + display_name=goal_cls.get_translatable_display_name(), + efficacy_specification=tuple( + IndicatorSpec(**indicator.to_dict()) + for indicator in goal_cls.get_efficacy_specification() + .get_indicators_specifications())) for _, strategy_cls in implemented_strategies.items(): strategies_map[strategy_cls.get_name()] = StrategyMapping( @@ -289,10 +300,12 @@ class Syncer(object): """ goal_display_name = goal_map.display_name goal_name = goal_map.name + goal_efficacy_spec = goal_map.efficacy_specification stale_goals = [] for matching_goal in matching_goals: - if matching_goal.display_name == goal_display_name: + if (matching_goal.efficacy_specification == goal_efficacy_spec and + matching_goal.display_name == goal_display_name): LOG.info(_LI("Goal %s unchanged"), goal_name) else: LOG.info(_LI("Goal %s modified"), goal_name) diff --git a/watcher/objects/goal.py b/watcher/objects/goal.py index 238632484..630c62337 100644 --- a/watcher/objects/goal.py +++ b/watcher/objects/goal.py @@ -32,6 +32,7 @@ class Goal(base.WatcherObject): 'uuid': obj_utils.str_or_none, 'name': obj_utils.str_or_none, 'display_name': obj_utils.str_or_none, + 'efficacy_specification': obj_utils.list_or_none, } @staticmethod diff --git a/watcher/tests/api/v1/test_goals.py b/watcher/tests/api/v1/test_goals.py index ba2d67fc5..611b35cc5 100644 --- a/watcher/tests/api/v1/test_goals.py +++ b/watcher/tests/api/v1/test_goals.py @@ -21,7 +21,8 @@ from watcher.tests.objects import utils as obj_utils class TestListGoal(api_base.FunctionalTest): def _assert_goal_fields(self, goal): - goal_fields = ['uuid', 'name', 'display_name'] + goal_fields = ['uuid', 'name', 'display_name', + 'efficacy_specification'] for field in goal_fields: self.assertIn(field, goal) diff --git a/watcher/tests/db/utils.py b/watcher/tests/db/utils.py index 8e42e1d5b..a76980a2f 100644 --- a/watcher/tests/db/utils.py +++ b/watcher/tests/db/utils.py @@ -151,6 +151,7 @@ def get_test_goal(**kwargs): 'created_at': kwargs.get('created_at'), 'updated_at': kwargs.get('updated_at'), 'deleted_at': kwargs.get('deleted_at'), + 'efficacy_specification': kwargs.get('efficacy_specification', []), } diff --git a/watcher/tests/decision_engine/fake_goals.py b/watcher/tests/decision_engine/fake_goals.py index e96bf26da..3e5618b07 100644 --- a/watcher/tests/decision_engine/fake_goals.py +++ b/watcher/tests/decision_engine/fake_goals.py @@ -14,7 +14,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import voluptuous + from watcher.decision_engine.goal import base as base_goal +from watcher.decision_engine.goal.efficacy import base as efficacy_base +from watcher.decision_engine.goal.efficacy import indicators +from watcher.decision_engine.goal.efficacy import specs class FakeGoal(base_goal.Goal): @@ -34,11 +39,44 @@ class FakeGoal(base_goal.Goal): def get_translatable_display_name(cls): return cls.DISPLAY_NAME + @classmethod + def get_efficacy_specification(cls): + """The efficacy spec for the current goal""" + return specs.Unclassified() + + +class DummyIndicator(indicators.IndicatorSpecification): + def __init__(self): + super(DummyIndicator, self).__init__( + name="dummy", + description="Dummy indicator", + unit="%", + ) + + @property + def schema(self): + return voluptuous.Schema( + voluptuous.Range(min=0, max=100), required=True) + + +class DummySpec1(efficacy_base.EfficacySpecification): + + def get_indicators_specifications(self): + return [DummyIndicator()] + + def get_global_efficacy_indicator(self, indicators_map): + return None + class FakeDummy1(FakeGoal): NAME = "dummy_1" DISPLAY_NAME = "Dummy 1" + @classmethod + def get_efficacy_specification(cls): + """The efficacy spec for the current goal""" + return DummySpec1() + class FakeDummy2(FakeGoal): NAME = "dummy_2" diff --git a/watcher/tests/decision_engine/test_sync.py b/watcher/tests/decision_engine/test_sync.py index 8471ea028..7c588e3df 100644 --- a/watcher/tests/decision_engine/test_sync.py +++ b/watcher/tests/decision_engine/test_sync.py @@ -49,6 +49,11 @@ class TestSyncer(base.DbTestCase): fake_goals.FakeDummy2.get_name(): fake_goals.FakeDummy2, }) + self.goal1_spec = fake_goals.FakeDummy1( + config=mock.Mock()).get_efficacy_specification() + self.goal2_spec = fake_goals.FakeDummy2( + config=mock.Mock()).get_efficacy_specification() + p_goals_load = mock.patch.object( default.DefaultGoalLoader, 'load', side_effect=lambda goal: self.m_available_goals()[goal]()) @@ -111,7 +116,9 @@ class TestSyncer(base.DbTestCase): objects.Goal(self.ctx, id=i) for i in range(1, 10)] m_g_list.return_value = [ objects.Goal(self.ctx, id=1, uuid=utils.generate_uuid(), - name="dummy_1", display_name="Dummy 1",) + name="dummy_1", display_name="Dummy 1", + efficacy_specification=( + self.goal1_spec.serialize_indicators_specs())) ] m_s_list.return_value = [] @@ -141,7 +148,9 @@ class TestSyncer(base.DbTestCase): objects.Goal(self.ctx, id=i) for i in range(1, 10)] m_g_list.return_value = [ objects.Goal(self.ctx, id=1, uuid=utils.generate_uuid(), - name="dummy_1", display_name="Dummy 1",) + name="dummy_1", display_name="Dummy 1", + efficacy_specification=( + self.goal1_spec.serialize_indicators_specs())) ] m_s_list.return_value = [ objects.Strategy(self.ctx, id=1, name="strategy_1", @@ -174,6 +183,7 @@ class TestSyncer(base.DbTestCase): m_g_list.return_value = [objects.Goal( self.ctx, id=1, uuid=utils.generate_uuid(), name="dummy_2", display_name="original", + efficacy_specification=self.goal2_spec.serialize_indicators_specs() )] m_s_list.return_value = [] self.syncer.sync() @@ -202,7 +212,9 @@ class TestSyncer(base.DbTestCase): objects.Goal(self.ctx, id=i) for i in range(1, 10)] m_g_list.return_value = [ objects.Goal(self.ctx, id=1, uuid=utils.generate_uuid(), - name="dummy_1", display_name="Dummy 1",) + name="dummy_1", display_name="Dummy 1", + efficacy_specification=( + self.goal1_spec.serialize_indicators_specs())) ] m_s_list.return_value = [ objects.Strategy(self.ctx, id=1, name="strategy_1", @@ -227,11 +239,14 @@ class TestSyncer(base.DbTestCase): # Should stay unmodified after sync() goal1 = objects.Goal( self.ctx, id=1, uuid=utils.generate_uuid(), - name="dummy_1", display_name="Dummy 1",) + name="dummy_1", display_name="Dummy 1", + efficacy_specification=( + self.goal1_spec.serialize_indicators_specs())) # Should be modified by the sync() goal2 = objects.Goal( self.ctx, id=2, uuid=utils.generate_uuid(), name="dummy_2", display_name="Original", + efficacy_specification=self.goal2_spec.serialize_indicators_specs() ) goal1.create() goal2.create() @@ -322,6 +337,16 @@ class TestSyncer(base.DbTestCase): if a_s.uuid not in [b_s.uuid for b_s in before_strategies] } + dummy_1_spec = [ + {'description': 'Dummy indicator', 'name': 'dummy', + 'schema': 'Range(min=0, max=100, min_included=True, ' + 'max_included=True, msg=None)', + 'unit': '%'}] + dummy_2_spec = [] + self.assertEqual( + [dummy_1_spec, dummy_2_spec], + [g.efficacy_specification for g in after_goals]) + self.assertEqual(1, len(created_goals)) self.assertEqual(3, len(created_strategies)) @@ -374,11 +399,13 @@ class TestSyncer(base.DbTestCase): goal1 = objects.Goal( self.ctx, id=1, uuid=utils.generate_uuid(), name="dummy_1", display_name="Dummy 1", + efficacy_specification=self.goal1_spec.serialize_indicators_specs() ) # To be removed by the sync() goal2 = objects.Goal( self.ctx, id=2, uuid=utils.generate_uuid(), name="dummy_2", display_name="Dummy 2", + efficacy_specification=self.goal2_spec.serialize_indicators_specs() ) goal1.create() goal2.create()