diff --git a/force_bdss/core_evaluation_driver.py b/force_bdss/core_evaluation_driver.py index 81291253edc09de89abbca4f9efc21680999e802..e10851c20d8849c78073c0ca642ce4d0c3a5aaf1 100644 --- a/force_bdss/core_evaluation_driver.py +++ b/force_bdss/core_evaluation_driver.py @@ -5,10 +5,6 @@ from traits.api import on_trait_change from force_bdss.ids import InternalPluginID from .base_core_driver import BaseCoreDriver -from .io.workflow_reader import ( - InvalidVersionException, - InvalidFileException -) log = logging.getLogger(__name__) @@ -24,8 +20,8 @@ class CoreEvaluationDriver(BaseCoreDriver): def application_started(self): try: workflow = self.workflow - except (InvalidVersionException, InvalidFileException) as e: - log.exception(e) + except Exception: + log.exception("Unable to open workflow file.") sys.exit(1) mco_model = workflow.mco diff --git a/force_bdss/factory_registry_plugin.py b/force_bdss/factory_registry_plugin.py index fb83b03edda16471198f497b2c630ac3324e89f3..cb6680b48b9b6af29cc0a79d2ce403a698fd1047 100644 --- a/force_bdss/factory_registry_plugin.py +++ b/force_bdss/factory_registry_plugin.py @@ -88,8 +88,7 @@ class FactoryRegistryPlugin(Plugin): if ds.id == id: return ds - raise KeyError("Unable to find data source factory with id {} " - "in the registry".format(id)) + raise KeyError(id) def mco_factory_by_id(self, id): """Finds a given Multi Criteria Optimizer (MCO) factory by means of @@ -109,8 +108,7 @@ class FactoryRegistryPlugin(Plugin): if mco.id == id: return mco - raise KeyError("Unable to find mco factory " - "with id {} in the registry.".format(id)) + raise KeyError(id) def mco_parameter_factory_by_id(self, mco_id, parameter_id): """Retrieves the MCO parameter factory for a given MCO id and @@ -138,8 +136,7 @@ class FactoryRegistryPlugin(Plugin): if factory.id == parameter_id: return factory - raise KeyError("Unable to find parameter factory with id {} " - "in the registry.".format(parameter_id)) + raise KeyError(parameter_id) def notification_listener_factory_by_id(self, id): """Finds a given notification listener by means of its id. @@ -159,5 +156,4 @@ class FactoryRegistryPlugin(Plugin): if nl.id == id: return nl - raise KeyError("Unable to find notification listener factory " - "with id {} in the registry".format(id)) + raise KeyError(id) diff --git a/force_bdss/io/tests/test_workflow_reader.py b/force_bdss/io/tests/test_workflow_reader.py index 7170e1e12968e2b384587d06eecf33832f53f978..84bd572a874aae30549a402ae58b4a04a6e754a0 100644 --- a/force_bdss/io/tests/test_workflow_reader.py +++ b/force_bdss/io/tests/test_workflow_reader.py @@ -4,23 +4,69 @@ from six import StringIO import testfixtures +from force_bdss.core.workflow import Workflow from force_bdss.io.workflow_reader import ( WorkflowReader, - InvalidVersionException, InvalidFileException) + InvalidVersionException, InvalidFileException, MissingPluginException, + ModelInstantiationFailedException) from force_bdss.tests.dummy_classes.factory_registry_plugin import \ DummyFactoryRegistryPlugin +from force_bdss.tests.probe_classes.factory_registry_plugin import \ + ProbeFactoryRegistryPlugin class TestWorkflowReader(unittest.TestCase): def setUp(self): self.registry = DummyFactoryRegistryPlugin() - self.wfreader = WorkflowReader(self.registry) + self.working_data = { + "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": {} + } + ] + }, + }, + "execution_layers": [ + [{ + "id": "force.bdss.enthought.plugin.test.v0" + ".factory.dummy_data_source", + "model_data": { + "input_slot_info": [], + "output_slot_info": [], + } + }], + ], + "notification_listeners": [ + { + "id": "force.bdss.enthought.plugin.test.v0" + ".factory.dummy_notification_listener", + "model_data": {} + }, + ] + } + } + def test_initialization(self): self.assertEqual(self.wfreader.factory_registry, self.registry) + workflow = self.wfreader.read( + _as_json_stringio(self.working_data) + ) + + self.assertIsInstance(workflow, Workflow) + def test_invalid_version(self): data = { "version": "2", @@ -29,7 +75,7 @@ class TestWorkflowReader(unittest.TestCase): with testfixtures.LogCapture(): with self.assertRaises(InvalidVersionException): - self.wfreader.read(self._as_json_stringio(data)) + self.wfreader.read(_as_json_stringio(data)) def test_absent_version(self): data = { @@ -37,7 +83,7 @@ class TestWorkflowReader(unittest.TestCase): with testfixtures.LogCapture(): with self.assertRaises(InvalidFileException): - self.wfreader.read(self._as_json_stringio(data)) + self.wfreader.read(_as_json_stringio(data)) def test_missing_key(self): data = { @@ -47,11 +93,117 @@ class TestWorkflowReader(unittest.TestCase): with testfixtures.LogCapture(): with self.assertRaises(InvalidFileException): - self.wfreader.read(self._as_json_stringio(data)) + self.wfreader.read(_as_json_stringio(data)) + + def test_missing_plugin_mco(self): + data = self.working_data + data["workflow"]["mco"]["id"] = "missing_mco" + + with self.assertRaises(MissingPluginException): + self.wfreader.read(_as_json_stringio(data)) + + def test_missing_plugin_mco_parameter(self): + data = self.working_data + data["workflow"]["mco"]["model_data"]["parameters"][0]["id"] = \ + "missing_parameter" + + with self.assertRaises(MissingPluginException): + self.wfreader.read(_as_json_stringio(data)) + + def test_missing_plugin_notification_listener(self): + data = self.working_data + data["workflow"]["notification_listeners"][0]["id"] = \ + "missing_nl" + + with self.assertRaises(MissingPluginException): + self.wfreader.read(_as_json_stringio(data)) + + def test_missing_plugin_data_source(self): + data = self.working_data + data["workflow"]["execution_layers"][0][0]["id"] = \ + "missing_ds" + + with self.assertRaises(MissingPluginException): + self.wfreader.read(_as_json_stringio(data)) + + +class TestModelCreationFailure(unittest.TestCase): + def setUp(self): + self.registry = ProbeFactoryRegistryPlugin() + self.wfreader = WorkflowReader(self.registry) + + self.working_data = { + "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": {} + } + ] + }, + }, + "execution_layers": [ + [{ + "id": "force.bdss.enthought.plugin.test.v0" + ".factory.probe_data_source", + "model_data": { + "input_slot_info": [], + "output_slot_info": [], + } + }], + ], + "notification_listeners": [ + { + "id": "force.bdss.enthought.plugin.test.v0" + ".factory.probe_notification_listener", + "model_data": {} + }, + ] + } + } + + def test_basic_probe_loading(self): + self.wfreader.read( + _as_json_stringio(self.working_data) + ) + + def test_data_source_model_throws(self): + self.registry.data_source_factories[0].raises_on_create_model = True + with testfixtures.LogCapture(): + with self.assertRaises(ModelInstantiationFailedException): + self.wfreader.read( + _as_json_stringio(self.working_data) + ) + + def test_mco_model_throws(self): + self.registry.mco_factories[0].raises_on_create_model = True + with testfixtures.LogCapture(): + with self.assertRaises(ModelInstantiationFailedException): + self.wfreader.read( + _as_json_stringio(self.working_data) + ) + + def test_notification_listener_throws(self): + factory = self.registry.notification_listener_factories[0] + factory.raises_on_create_model = True + + with testfixtures.LogCapture(): + with self.assertRaises(ModelInstantiationFailedException): + self.wfreader.read( + _as_json_stringio(self.working_data) + ) + - def _as_json_stringio(self, data): - fp = StringIO() - json.dump(data, fp) - fp.seek(0) +def _as_json_stringio(data): + fp = StringIO() + json.dump(data, fp) + fp.seek(0) - return fp + return fp diff --git a/force_bdss/io/workflow_reader.py b/force_bdss/io/workflow_reader.py index 4f6ed89303e830650c977cc9172b14ee0559f936..dd9687e2f28c87696caa8e4f047fbfcd60e3fc56 100644 --- a/force_bdss/io/workflow_reader.py +++ b/force_bdss/io/workflow_reader.py @@ -9,20 +9,34 @@ from force_bdss.core.output_slot_info import OutputSlotInfo from force_bdss.core.workflow import Workflow from ..factory_registry_plugin import IFactoryRegistryPlugin -log = logging.getLogger(__name__) +logger = logging.getLogger(__name__) SUPPORTED_FILE_VERSIONS = ["1"] -class InvalidFileException(Exception): - """Raised if the file is invalid for some reason""" +class BaseWorkflowReaderException(Exception): + """Base exception for the reader errors.""" -class InvalidVersionException(InvalidFileException): +class InvalidFileException(BaseWorkflowReaderException): + """Raised for a generic file being invalid for some + reason, e.g. incorrect format or missing keys. + """ + + +class InvalidVersionException(BaseWorkflowReaderException): """Raised if the version tag does not satisfy the currently supported list.""" +class MissingPluginException(BaseWorkflowReaderException): + """Raised if the file requires a plugin we cannot find.""" + + +class ModelInstantiationFailedException(BaseWorkflowReaderException): + """Raised if we can't instantiate the model from a plugin""" + + class WorkflowReader(HasStrictTraits): """ Reads the workflow from a file. @@ -67,18 +81,30 @@ class WorkflowReader(HasStrictTraits): ------ InvalidFileException Raised if the file is corrupted or cannot be read by this reader. + + InvalidVersionException + Raised if the version is not supported. + + MissingPluginException + The file cannot be opened because it needs a plugin that is not + available. + + ModelInstantiationFailedException + When instantiating the model for a given plugin, an exception + occurred. This is likely due to a coding error in the plugin. + """ json_data = json.load(file) try: version = json_data["version"] except KeyError: - log.error("File missing version information") + logger.error("File missing version information") raise InvalidFileException("Corrupted input file, no version" " specified") if version not in SUPPORTED_FILE_VERSIONS: - log.error( + logger.error( "File contains version {} that is not in the " "list of supported versions {}".format( version, SUPPORTED_FILE_VERSIONS) @@ -95,13 +121,13 @@ class WorkflowReader(HasStrictTraits): wf.notification_listeners[:] = \ self._extract_notification_listeners(wf_data) except KeyError as e: - log.error("Could not read file {}".format(file), exc_info=True) - raise InvalidFileException( + msg = ( "Could not read file {}. " - "Unable to find key {}." - "The plugin responsible for the missing " - "key may be missing or broken.".format(file, e) - ) + "Unable to find key {}. " + "The file might be corrupted or unsupported.".format(file, e)) + logger.exception(msg) + raise InvalidFileException(msg) + return wf def _extract_mco(self, wf_data): @@ -127,13 +153,30 @@ class WorkflowReader(HasStrictTraits): return None mco_id = mco_data["id"] - mco_factory = registry.mco_factory_by_id(mco_id) + try: + mco_factory = registry.mco_factory_by_id(mco_id) + except KeyError: + raise MissingPluginException( + "Could not read file. " + "The plugin responsible for the missing " + "key '{}' may be missing or broken.".format(mco_id) + ) model_data = wf_data["mco"]["model_data"] model_data["parameters"] = self._extract_mco_parameters( mco_id, model_data["parameters"]) - model = mco_factory.create_model( - wf_data["mco"]["model_data"]) + + try: + model = mco_factory.create_model(model_data) + except Exception as e: + msg = ( + "Unable to create model for MCO {}: {}. " + "This is likely due to a coding error in the plugin. " + "Check the logs for more information.".format( + mco_id, e)) + + logger.exception(msg) + raise ModelInstantiationFailedException(msg) return model def _extract_execution_layers(self, wf_data): @@ -157,7 +200,16 @@ class WorkflowReader(HasStrictTraits): for ds_entry in el_entry: ds_id = ds_entry["id"] - ds_factory = registry.data_source_factory_by_id(ds_id) + + try: + ds_factory = registry.data_source_factory_by_id(ds_id) + except KeyError: + raise MissingPluginException( + "Could not read file. " + "The plugin responsible for the missing data source " + "key '{}' may be missing or broken.".format(ds_id) + ) + model_data = ds_entry["model_data"] model_data["input_slot_info"] = self._extract_input_slot_info( model_data["input_slot_info"] @@ -166,8 +218,21 @@ class WorkflowReader(HasStrictTraits): self._extract_output_slot_info( model_data["output_slot_info"] ) - layer.data_sources.append( - ds_factory.create_model(model_data)) + + try: + ds_model = ds_factory.create_model(model_data) + except Exception as e: + msg = ( + "Unable to create model for DataSource {} : {}. " + "This is likely due to a coding " + "error in the plugin. Check the logs for more " + "information.".format(ds_id, e) + ) + logger.exception(msg) + raise ModelInstantiationFailedException(msg) + + layer.data_sources.append(ds_model) + layers.append(layer) return layers @@ -189,9 +254,29 @@ class WorkflowReader(HasStrictTraits): parameters = [] for p in parameters_data: - id = p["id"] - factory = registry.mco_parameter_factory_by_id(mco_id, id) - model = factory.create_model(p["model_data"]) + parameter_id = p["id"] + try: + factory = registry.mco_parameter_factory_by_id( + mco_id, parameter_id) + except KeyError: + raise MissingPluginException( + "Could not read file. " + "The plugin responsible for the missing MCO '{}' " + "parameter key '{}' may be missing or broken.".format( + mco_id, parameter_id) + ) + + try: + model = factory.create_model(p["model_data"]) + except Exception as e: + msg = ( + "Unable to create model for MCO {} parameter {} : {}. " + "This is likely due to an error in the plugin. " + "Check the logs for more information.".format( + mco_id, parameter_id, e)) + logger.exception(msg) + raise ModelInstantiationFailedException(msg) + parameters.append(model) return parameters @@ -207,8 +292,29 @@ class WorkflowReader(HasStrictTraits): listeners = [] for nl_entry in wf_data["notification_listeners"]: nl_id = nl_entry["id"] - nl_factory = registry.notification_listener_factory_by_id(nl_id) + try: + nl_factory = registry.notification_listener_factory_by_id( + nl_id) + except KeyError: + raise MissingPluginException( + "Could not read file. " + "The plugin responsible for the missing " + "notification listener key '{}' may be missing " + "or broken.".format(nl_id) + ) + model_data = nl_entry["model_data"] - listeners.append(nl_factory.create_model(model_data)) + + try: + model = nl_factory.create_model(model_data) + except Exception as e: + msg = ( + "Unable to create model for Notification Listener " + "{} : {}. This is likely due to an error in the plugin. " + "Check the logs for more information.".format(nl_id, e)) + logger.exception(msg) + raise ModelInstantiationFailedException(msg) + + listeners.append(model) return listeners diff --git a/force_bdss/tests/fixtures/test_null.json b/force_bdss/tests/fixtures/test_null.json index b40385023049b793d3d74b7285135dc42cfadcc2..b5c9ab99b836a09c01b1c196165d73fd59b6f63e 100644 --- a/force_bdss/tests/fixtures/test_null.json +++ b/force_bdss/tests/fixtures/test_null.json @@ -2,7 +2,7 @@ "version": "1", "workflow": { "mco": { - "id": "force.bdss.enthought.plugin.test.v0.factory.test_mco", + "id": "force.bdss.enthought.plugin.test.v0.factory.probe_mco", "model_data": { "parameters" : [ ] @@ -11,7 +11,7 @@ "execution_layers": [ [ { - "id": "force.bdss.enthought.plugin.test.v0.factory.test_ds", + "id": "force.bdss.enthought.plugin.test.v0.factory.probe_data_source", "model_data": { "input_slot_info": [ ], @@ -23,7 +23,7 @@ ], "notification_listeners": [ { - "id": "force.bdss.enthought.plugin.test.v0.factory.test_nl", + "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 5b6119ead303e72991805dc302d84c920333ee27..65340f1df98a4923d1ff3fe2d8d4937423bad2a3 100644 --- a/force_bdss/tests/probe_classes/data_source.py +++ b/force_bdss/tests/probe_classes/data_source.py @@ -54,8 +54,11 @@ class ProbeDataSourceFactory(BaseDataSourceFactory): input_slots_size = Int(0) output_slots_size = Int(0) + raises_on_create_model = Bool(False) + raises_on_create_data_source = Bool(False) + def get_identifier(self): - return "test_ds" + return "probe_data_source" def get_name(self): return "test_data_source" @@ -67,6 +70,9 @@ class ProbeDataSourceFactory(BaseDataSourceFactory): return ProbeDataSource def create_model(self, model_data=None): + if self.raises_on_create_model: + raise Exception("ProbeDataSourceFactory.create_model") + if model_data is None: model_data = {} return self.model_class( @@ -79,6 +85,9 @@ class ProbeDataSourceFactory(BaseDataSourceFactory): ) def create_data_source(self): + if self.raises_on_create_data_source: + raise Exception("ProbeDataSourceFactory.create_data_source") + return self.data_source_class( factory=self, run_function=self.run_function, diff --git a/force_bdss/tests/probe_classes/mco.py b/force_bdss/tests/probe_classes/mco.py index 3b7bca244dbad8694d2f76a0bec3674387e60495..0c76d20491b0c67e207473853a87dd52e038fd1c 100644 --- a/force_bdss/tests/probe_classes/mco.py +++ b/force_bdss/tests/probe_classes/mco.py @@ -39,7 +39,7 @@ class ProbeParameterFactory(BaseMCOParameterFactory): return "Probe parameter" def get_identifier(self): - return "probe_parameter" + return "probe_mco_parameter" def get_description(self): return "Probe parameter" @@ -67,8 +67,12 @@ class ProbeMCOCommunicator(BaseMCOCommunicator): class ProbeMCOFactory(BaseMCOFactory): nb_output_data_values = Int(0) + raises_on_create_model = Bool(False) + raises_on_create_optimizer = Bool(False) + raises_on_create_communicator = Bool(False) + def get_identifier(self): - return "test_mco" + return "probe_mco" def get_model_class(self): return ProbeMCOModel @@ -83,9 +87,27 @@ class ProbeMCOFactory(BaseMCOFactory): return "testmco" def create_communicator(self): + if self.raises_on_create_communicator: + raise Exception("ProbeMCOFactory.create_communicator") + return self.communicator_class( self, nb_output_data_values=self.nb_output_data_values) + def create_model(self, model_data): + if self.raises_on_create_model: + raise Exception("ProbeMCOFactory.create_model") + + if model_data is None: + model_data = {} + + return self.model_class(self, **model_data) + + def create_optimizer(self): + if self.raises_on_create_optimizer: + raise Exception("ProbeMCOFactory.create_optimizer") + + return self.optimizer_class(self) + def parameter_factories(self): return [ProbeParameterFactory(mco_factory=self)] diff --git a/force_bdss/tests/probe_classes/notification_listener.py b/force_bdss/tests/probe_classes/notification_listener.py index 5de05f50c9a4a7d16879db9cc4271f6efa345d44..3e3c987b26d5730671a4e46897cb649cabf897ce 100644 --- a/force_bdss/tests/probe_classes/notification_listener.py +++ b/force_bdss/tests/probe_classes/notification_listener.py @@ -46,11 +46,14 @@ class ProbeNotificationListenerFactory(BaseNotificationListenerFactory): deliver_function = Function(default_value=pass_function) finalize_function = Function(default_value=pass_function) + raises_on_create_model = Bool(False) + raises_on_create_listener = Bool(False) + def get_name(self): return "test_notification_listener" def get_identifier(self): - return "test_nl" + return "probe_notification_listener" def get_listener_class(self): return ProbeNotificationListener @@ -58,7 +61,19 @@ class ProbeNotificationListenerFactory(BaseNotificationListenerFactory): def get_model_class(self): return ProbeNotificationListenerModel + def create_model(self, model_data=None): + if self.raises_on_create_model: + raise Exception("ProbeNotificationListenerFactory.create_model") + + if model_data is None: + model_data = {} + + return self.model_class(self, **model_data) + def create_listener(self): + if self.raises_on_create_listener: + raise Exception("ProbeNotificationListenerFactory.create_listener") + return self.listener_class( self, initialize_function=self.initialize_function, diff --git a/force_bdss/tests/test_core_mco_driver.py b/force_bdss/tests/test_core_mco_driver.py index c2f2452e642cc9c12c72d0f7059cc57fed6a15df..d14fbfe4785cf22d948b129a7b6c0642aede370d 100644 --- a/force_bdss/tests/test_core_mco_driver.py +++ b/force_bdss/tests/test_core_mco_driver.py @@ -92,7 +92,8 @@ class TestCoreMCODriver(unittest.TestCase): ("force_bdss.core_mco_driver", "ERROR", "Failed to create or initialize listener with id " - "force.bdss.enthought.plugin.test.v0.factory.test_nl: ")) + "force.bdss.enthought.plugin.test.v0" + ".factory.probe_notification_listener: ")) self.assertEqual(len(listeners), 0)