diff --git a/force_bdss/cli/tests/test_execution.py b/force_bdss/cli/tests/test_execution.py index 3202beee1ddbe86d1e31833a86a09ae16770b554..c6cdd6a34b6368af3275c0b110e428ca1ccf7e1e 100644 --- a/force_bdss/cli/tests/test_execution.py +++ b/force_bdss/cli/tests/test_execution.py @@ -26,7 +26,7 @@ class TestExecution(unittest.TestCase): def test_plain_invocation_mco(self): with cd(fixtures.dirpath()): try: - subprocess.check_output(["force_bdss", "test_empty.json"], + subprocess.check_output(["force_bdss", '--help'], stderr=subprocess.STDOUT) except subprocess.CalledProcessError: self.fail("force_bdss returned error at plain invocation.") diff --git a/force_bdss/core/tests/test_verifier.py b/force_bdss/core/tests/test_verifier.py new file mode 100644 index 0000000000000000000000000000000000000000..6c5ea3569d928820a4469f4f2154a18040aa92b2 --- /dev/null +++ b/force_bdss/core/tests/test_verifier.py @@ -0,0 +1,92 @@ +import unittest + +from force_bdss.core.execution_layer import ExecutionLayer +from force_bdss.core.input_slot_info import InputSlotInfo +from force_bdss.core.output_slot_info import OutputSlotInfo +from force_bdss.core.verifier import verify_workflow +from force_bdss.core.workflow import Workflow +from force_bdss.tests.dummy_classes.extension_plugin import \ + DummyExtensionPlugin + + +class TestVerifier(unittest.TestCase): + def setUp(self): + self.plugin = DummyExtensionPlugin() + self.workflow = Workflow() + + def test_empty_workflow(self): + wf = self.workflow + errors = verify_workflow(wf) + self.assertEqual(len(errors), 2) + self.assertEqual(errors[0].subject, wf) + self.assertIn("no MCO", errors[0].error) + + self.assertEqual(errors[1].subject, wf) + self.assertIn("no execution layers", errors[1].error) + + def test_no_mco_parameters(self): + wf = self.workflow + wf.mco = self.plugin.mco_factories[0].create_model() + + errors = verify_workflow(wf) + self.assertEqual(len(errors), 2) + self.assertEqual(errors[0].subject, wf.mco) + self.assertIn("no defined parameters", errors[0].error) + + def test_parameters_empty_names(self): + wf = self.workflow + mco_factory = self.plugin.mco_factories[0] + wf.mco = mco_factory.create_model() + parameter_factory = mco_factory.parameter_factories()[0] + wf.mco.parameters.append(parameter_factory.create_model()) + + errors = verify_workflow(wf) + self.assertEqual(len(errors), 3) + self.assertEqual(errors[0].subject, wf.mco.parameters[0]) + self.assertIn("empty name", errors[0].error) + self.assertEqual(errors[1].subject, wf.mco.parameters[0]) + self.assertIn("empty type", errors[1].error) + + def test_data_sources(self): + wf = self.workflow + mco_factory = self.plugin.mco_factories[0] + wf.mco = mco_factory.create_model() + parameter_factory = mco_factory.parameter_factories()[0] + wf.mco.parameters.append(parameter_factory.create_model()) + wf.mco.parameters[0].name = "name" + wf.mco.parameters[0].type = "type" + + layer = ExecutionLayer() + wf.execution_layers.append(layer) + ds_factory = self.plugin.data_source_factories[0] + ds_model = ds_factory.create_model() + layer.data_sources.append(ds_model) + + errors = verify_workflow(wf) + self.assertEqual(errors[0].subject, ds_model) + self.assertIn("Missing input slot name assignment", errors[0].error) + + ds_model.input_slot_info.append( + InputSlotInfo(name="name") + ) + + errors = verify_workflow(wf) + self.assertEqual(errors[0].subject, ds_model) + self.assertIn("Missing output slot name assignment", errors[0].error) + + ds_model.output_slot_info.append( + OutputSlotInfo(name="name") + ) + + errors = verify_workflow(wf) + self.assertEqual(len(errors), 0) + + ds_model.input_slot_info[0].name = '' + errors = verify_workflow(wf) + self.assertEqual(len(errors), 1) + self.assertIn("Undefined name for input parameter", errors[0].error) + + ds_model.output_slot_info[0].name = '' + errors = verify_workflow(wf) + self.assertEqual(len(errors), 2) + self.assertIn("Undefined name for output parameter", errors[1].error) diff --git a/force_bdss/core/verifier.py b/force_bdss/core/verifier.py new file mode 100644 index 0000000000000000000000000000000000000000..3361f35fc143a2b0b9bac09e1ca594cc548e57e3 --- /dev/null +++ b/force_bdss/core/verifier.py @@ -0,0 +1,122 @@ +import logging +from traits.api import HasStrictTraits, Str, Any + +logger = logging.getLogger(__name__) + + +class VerifierError(HasStrictTraits): + subject = Any() + error = Str() + + +def verify_workflow(workflow): + """Verifies if the workflow can be executed, and specifies where the + error occurs and why. + + """ + result = [] + + result.extend(_check_mco(workflow)) + result.extend(_check_execution_layers(workflow)) + + return result + + +def _check_mco(workflow): + errors = [] + if workflow.mco is None: + errors.append( + VerifierError( + subject=workflow, + error="Workflow has no MCO" + ) + ) + return errors + + mco = workflow.mco + if len(mco.parameters) == 0: + errors.append(VerifierError(subject=mco, + error="MCO has no defined parameters")) + + for param in mco.parameters: + if len(param.name.strip()) == 0: + errors.append(VerifierError(subject=param, + error="Parameter has empty name")) + if len(param.type.strip()) == 0: + errors.append(VerifierError(subject=param, + error="Parameter has empty type")) + return errors + + +def _check_execution_layers(workflow): + errors = [] + + layers = workflow.execution_layers + if len(layers) == 0: + errors.append( + VerifierError( + subject=workflow, + error="Workflow has no execution layers" + ) + ) + + return errors + + for layer in layers: + if len(layer.data_sources) == 0: + errors.append(VerifierError(subject=layer, + error="Layer has no data sources")) + + for ds in layer.data_sources: + errors.extend(_check_data_source(ds)) + + return errors + + +def _check_data_source(data_source_model): + errors = [] + + factory = data_source_model.factory + try: + data_source = factory.create_data_source() + except Exception: + logger.exception("Unable to create data source from factory" + " '{}', plugin '{}'. This might indicate a " + "programming error".format(factory.id, + factory.plugin.id)) + raise + + try: + input_slots, output_slots = data_source.slots(data_source_model) + except Exception: + logger.exception( + "Unable to retrieve slot information from data source" + " created by factory '{}', plugin '{}'. This might " + "indicate a programming error.".format( + data_source.factory.id, + data_source.factory.plugin.id)) + raise + + if len(input_slots) != len(data_source_model.input_slot_info): + errors.append(VerifierError( + subject=data_source_model, + error="Missing input slot name assignment")) + + for idx, info in enumerate(data_source_model.input_slot_info): + if len(info.name.strip()) == 0: + errors.append(VerifierError( + subject=data_source_model, + error="Undefined name for input parameter {}".format(idx))) + + if len(output_slots) != len(data_source_model.output_slot_info): + errors.append(VerifierError( + subject=data_source_model, + error="Missing output slot name assignment")) + + for idx, info in enumerate(data_source_model.output_slot_info): + if len(info.name.strip()) == 0: + errors.append(VerifierError( + subject=data_source_model, + error="Undefined name for output parameter {}".format(idx))) + + return errors diff --git a/force_bdss/core_mco_driver.py b/force_bdss/core_mco_driver.py index c0ff9fa016664685d4607612a4425129f764b084..733b34b8a07c06a662c72b8f7c25bf0b7729071f 100644 --- a/force_bdss/core_mco_driver.py +++ b/force_bdss/core_mco_driver.py @@ -3,6 +3,7 @@ import logging from traits.api import on_trait_change, Instance, List +from force_bdss.core.verifier import verify_workflow from force_bdss.ids import InternalPluginID from force_bdss.mco.base_mco import BaseMCO from force_bdss.notification_listeners.base_notification_listener import \ @@ -25,6 +26,21 @@ class CoreMCODriver(BaseCoreDriver): @on_trait_change("application:started") def application_started(self): + try: + workflow = self.workflow + except Exception: + log.exception("Unable to open workflow file.") + sys.exit(1) + + errors = verify_workflow(workflow) + + if len(errors) != 0: + log.error("Unable to execute workflow due to verification " + "errors :") + for err in errors: + log.error(err.error) + sys.exit(1) + try: mco = self.mco except Exception: @@ -51,13 +67,8 @@ class CoreMCODriver(BaseCoreDriver): self.listeners[:] = [] def _mco_default(self): - try: - workflow = self.workflow - except Exception: - log.exception("Unable to open workflow file.") - raise - mco_model = workflow.mco + mco_model = self.workflow.mco if mco_model is None: log.info("No MCO defined. Nothing to do. Exiting.") sys.exit(0) diff --git a/force_bdss/tests/dummy_classes/data_source.py b/force_bdss/tests/dummy_classes/data_source.py index b53cb5cbcc6aff2689ffe790f2d84f2a1d372fc7..c4547881d5edae073c31044feeaf7b2c4b852406 100644 --- a/force_bdss/tests/dummy_classes/data_source.py +++ b/force_bdss/tests/dummy_classes/data_source.py @@ -1,3 +1,4 @@ +from force_bdss.core.slot import Slot from force_bdss.data_sources.base_data_source import BaseDataSource from force_bdss.data_sources.base_data_source_factory import \ BaseDataSourceFactory @@ -9,7 +10,11 @@ class DummyDataSource(BaseDataSource): pass def slots(self, model): - return (), () + return ( + Slot(type="TYPE1"), + ), ( + Slot(type="TYPE2"), + ) class DummyDataSourceModel(BaseDataSourceModel): diff --git a/force_bdss/tests/fixtures/test_dummy.json b/force_bdss/tests/fixtures/test_dummy.json new file mode 100644 index 0000000000000000000000000000000000000000..36ec5c0587db0f2d7d2bd06170c0b98f598a3a83 --- /dev/null +++ b/force_bdss/tests/fixtures/test_dummy.json @@ -0,0 +1,45 @@ +{ + "version": "1", + "workflow": { + "mco": { + "id": "force.bdss.enthought.plugin.test.v0.factory.dummy_mco", + "model_data": { + "parameters": [ + { + "id": "force.bdss.enthought.plugin.test.v0.factory.dummy_mco.parameter.dummy_mco_parameter", + "model_data": { + "name": "foo", + "type": "PRESSURE" + } + } + ] + } + }, + "execution_layers": [ + [ + { + "id": "force.bdss.enthought.plugin.test.v0.factory.dummy_data_source", + "model_data": { + "power": 1.0, + "cuba_type_in": "PRESSURE", + "cuba_type_out": "PRESSURE", + "input_slot_info": [ + { + "source": "Environment", + "name": "foo" + } + ], + "output_slot_info": [ + { + "name": "bar", + "is_kpi": true + } + ] + } + } + ] + ], + "notification_listeners": [ + ] + } +} diff --git a/force_bdss/tests/fixtures/test_probe.json b/force_bdss/tests/fixtures/test_probe.json new file mode 100644 index 0000000000000000000000000000000000000000..2eba16d229b756c737c537fc36cd3cd61bd3e528 --- /dev/null +++ b/force_bdss/tests/fixtures/test_probe.json @@ -0,0 +1,47 @@ +{ + "version": "1", + "workflow": { + "mco": { + "id": "force.bdss.enthought.plugin.test.v0.factory.probe_mco", + "model_data": { + "parameters": [ + { + "id": "force.bdss.enthought.plugin.test.v0.factory.probe_mco.parameter.probe_mco_parameter", + "model_data": { + "name": "foo", + "type": "PRESSURE" + } + } + ] + } + }, + "execution_layers": [ + [ + { + "id": "force.bdss.enthought.plugin.test.v0.factory.probe_data_source", + "model_data": { + "input_slot_info": [ + { + "source": "Environment", + "name": "foo" + } + ], + "output_slot_info": [ + { + "name": "bar", + "is_kpi": true + } + ] + } + } + ] + ], + "notification_listeners": [ + { + "id": "force.bdss.enthought.plugin.test.v0.factory.probe_notification_listener", + "model_data": { + } + } + ] + } +} diff --git a/force_bdss/tests/probe_classes/data_source.py b/force_bdss/tests/probe_classes/data_source.py index 65340f1df98a4923d1ff3fe2d8d4937423bad2a3..b2f83cbfead608b590e17e9e7dcc06a919b4a71e 100644 --- a/force_bdss/tests/probe_classes/data_source.py +++ b/force_bdss/tests/probe_classes/data_source.py @@ -4,10 +4,11 @@ from force_bdss.api import ( BaseDataSourceFactory, BaseDataSourceModel, BaseDataSource, Slot ) +from force_bdss.core.data_value import DataValue -def run_func(*args, **kwargs): - return [] +def run_func(model, parameters): + return [DataValue() for _ in range(model.output_slots_size)] class ProbeDataSource(BaseDataSource): @@ -35,8 +36,8 @@ class ProbeDataSourceModel(BaseDataSourceModel): input_slots_type = Str('PRESSURE') output_slots_type = Str('PRESSURE') - input_slots_size = Int(0) - output_slots_size = Int(0) + input_slots_size = Int(1) + output_slots_size = Int(1) @on_trait_change('input_slots_type,output_slots_type,' 'input_slots_size,output_slots_size') @@ -51,8 +52,8 @@ class ProbeDataSourceFactory(BaseDataSourceFactory): input_slots_type = Str('PRESSURE') output_slots_type = Str('PRESSURE') - input_slots_size = Int(0) - output_slots_size = Int(0) + input_slots_size = Int(1) + output_slots_size = Int(1) raises_on_create_model = Bool(False) raises_on_create_data_source = Bool(False) diff --git a/force_bdss/tests/probe_classes/mco.py b/force_bdss/tests/probe_classes/mco.py index 905b03cf2e93af290c20d667e6917257d54deccf..1c84b8ee9c350f6df9652fe8136acce8f4cbcb29 100644 --- a/force_bdss/tests/probe_classes/mco.py +++ b/force_bdss/tests/probe_classes/mco.py @@ -52,7 +52,7 @@ class ProbeMCOCommunicator(BaseMCOCommunicator): send_called = Bool(False) receive_called = Bool(False) - nb_output_data_values = Int(0) + nb_output_data_values = Int(1) def send_to_mco(self, model, kpi_results): self.send_called = True @@ -60,12 +60,13 @@ class ProbeMCOCommunicator(BaseMCOCommunicator): def receive_from_mco(self, model): self.receive_called = True return [ - DataValue() for _ in range(self.nb_output_data_values) + DataValue(name="whatever", value=1.0) + for _ in range(self.nb_output_data_values) ] class ProbeMCOFactory(BaseMCOFactory): - nb_output_data_values = Int(0) + nb_output_data_values = Int(1) raises_on_create_model = Bool(False) raises_on_create_optimizer = Bool(False) diff --git a/force_bdss/tests/test_core_evaluation_driver.py b/force_bdss/tests/test_core_evaluation_driver.py index b3a0b96b9b014d50dc0d7245e5d0b777c26db410..5d206ecdbce91993263203828b30753d2e84358d 100644 --- a/force_bdss/tests/test_core_evaluation_driver.py +++ b/force_bdss/tests/test_core_evaluation_driver.py @@ -38,7 +38,7 @@ class TestCoreEvaluationDriver(unittest.TestCase): application.get_plugin = mock.Mock( return_value=self.registry ) - application.workflow_filepath = fixtures.get("test_null.json") + application.workflow_filepath = fixtures.get("test_probe.json") self.mock_application = application def test_initialization(self): @@ -49,7 +49,7 @@ class TestCoreEvaluationDriver(unittest.TestCase): def test_error_for_non_matching_mco_parameters(self): mco_factory = self.registry.mco_factories[0] - mco_factory.nb_output_data_values = 1 + mco_factory.nb_output_data_values = 2 driver = CoreEvaluationDriver( application=self.mock_application) with testfixtures.LogCapture(): @@ -62,7 +62,7 @@ class TestCoreEvaluationDriver(unittest.TestCase): def test_error_for_incorrect_output_slots(self): def run(self, *args, **kwargs): - return [DataValue()] + return [DataValue(), DataValue()] ds_factory = self.registry.data_source_factories[0] ds_factory.run_function = run driver = CoreEvaluationDriver(application=self.mock_application) @@ -70,7 +70,7 @@ class TestCoreEvaluationDriver(unittest.TestCase): with six.assertRaisesRegex( self, RuntimeError, - "The number of data values \(1 values\)" + "The number of data values \(2 values\)" " returned by 'test_data_source' does not match" " the number of output slots"): driver.application_started() @@ -78,11 +78,11 @@ class TestCoreEvaluationDriver(unittest.TestCase): def test_error_for_missing_ds_output_names(self): def run(self, *args, **kwargs): - return [DataValue()] + return [DataValue(), DataValue()] ds_factory = self.registry.data_source_factories[0] ds_factory.run_function = run - ds_factory.output_slots_size = 1 + ds_factory.output_slots_size = 2 driver = CoreEvaluationDriver( application=self.mock_application, ) @@ -90,7 +90,7 @@ class TestCoreEvaluationDriver(unittest.TestCase): with six.assertRaisesRegex( self, RuntimeError, - "The number of data values \(1 values\)" + "The number of data values \(2 values\)" " returned by 'test_data_source' does not match" " the number of user-defined names"): driver.application_started() @@ -323,7 +323,7 @@ class TestCoreEvaluationDriver(unittest.TestCase): ('force_bdss.core_evaluation_driver', 'INFO', 'Creating communicator'), ('force_bdss.core_evaluation_driver', 'INFO', - 'Received data from MCO: \n'), + 'Received data from MCO: \n whatever = 1.0 (AVERAGE)'), ('force_bdss.core_evaluation_driver', 'INFO', 'Computing data layer 0'), ('force_bdss.core_evaluation_driver', 'ERROR', diff --git a/force_bdss/tests/test_core_mco_driver.py b/force_bdss/tests/test_core_mco_driver.py index 1c09a295c100a65ed2448ada3b145d63bcd2b9d5..d137b2122c554233e3d8cf29273cf544a8a03500 100644 --- a/force_bdss/tests/test_core_mco_driver.py +++ b/force_bdss/tests/test_core_mco_driver.py @@ -28,7 +28,7 @@ class TestCoreMCODriver(unittest.TestCase): application.get_plugin = mock.Mock( return_value=self.factory_registry_plugin ) - application.workflow_filepath = fixtures.get("test_null.json") + application.workflow_filepath = fixtures.get("test_probe.json") self.mock_application = application def test_initialization(self): @@ -202,3 +202,37 @@ class TestCoreMCODriver(unittest.TestCase): "'force.bdss.enthought.plugin.test.v0'" " raised exception. This might indicate " 'a programming error in the plugin.'),) + + def test_nonexistent_file(self): + self.mock_application.workflow_filepath = fixtures.get( + "test_nonexistent.json") + driver = CoreMCODriver( + application=self.mock_application, + ) + with LogCapture() as capture: + with self.assertRaises(SystemExit): + driver.application_started() + capture.check( + ('force_bdss.core_mco_driver', 'ERROR', + 'Unable to open workflow file.'), + ) + + def test_non_valid_file(self): + self.mock_application.workflow_filepath = fixtures.get( + "test_null.json") + driver = CoreMCODriver( + application=self.mock_application, + ) + with LogCapture() as capture: + with self.assertRaises(SystemExit): + driver.application_started() + capture.check( + ('force_bdss.core_mco_driver', + 'ERROR', + 'Unable to execute workflow due to verification errors :'), + ('force_bdss.core_mco_driver', 'ERROR', + 'MCO has no defined parameters'), + ('force_bdss.core_mco_driver', 'ERROR', + 'Missing input slot name assignment'), + ('force_bdss.core_mco_driver', 'ERROR', + 'Missing output slot name assignment'))