diff --git a/.gitignore b/.gitignore index 8bd33662..a5618684 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,4 @@ dmypy.json # test files input_file.yaml **/*.nc +!tests/**/*.nc diff --git a/decoimpact/business/entities/rules/multiply_rule.py b/decoimpact/business/entities/rules/multiply_rule.py index 8fb3459b..176d5849 100644 --- a/decoimpact/business/entities/rules/multiply_rule.py +++ b/decoimpact/business/entities/rules/multiply_rule.py @@ -22,8 +22,9 @@ def __init__( input_variable_names: List[str], multipliers: List[float], output_variable_name: str = "output", + description: str = "", ): - super().__init__(name, input_variable_names, output_variable_name) + super().__init__(name, input_variable_names, output_variable_name, description) self._multipliers = multipliers @property diff --git a/decoimpact/business/entities/rules/rule_base.py b/decoimpact/business/entities/rules/rule_base.py index 58f2b3e4..14e84c16 100644 --- a/decoimpact/business/entities/rules/rule_base.py +++ b/decoimpact/business/entities/rules/rule_base.py @@ -20,10 +20,11 @@ def __init__( name: str, input_variable_names: List[str], output_variable_name: str = "output", + description: str = "" ): self._name = name - self._description = "" + self._description = description self._input_variable_names = input_variable_names self._output_variable_name = output_variable_name diff --git a/decoimpact/data/api/i_rule_data.py b/decoimpact/data/api/i_rule_data.py index 3a39acab..2eab827e 100644 --- a/decoimpact/data/api/i_rule_data.py +++ b/decoimpact/data/api/i_rule_data.py @@ -17,6 +17,11 @@ class IRuleData(ABC): def name(self) -> str: """Name of the rule""" + @property + @abstractmethod + def description(self) -> str: + """Description of the rule""" + @property @abstractmethod def output_variable(self) -> str: diff --git a/decoimpact/data/entities/dataset_data.py b/decoimpact/data/entities/dataset_data.py index 32f9735e..3a249765 100644 --- a/decoimpact/data/entities/dataset_data.py +++ b/decoimpact/data/entities/dataset_data.py @@ -22,7 +22,7 @@ def __init__(self, dataset: dict[str, Any]): """Create DatasetData based on provided info dictionary Args: - info (dict[str, Any]): + dataset (dict[str, Any]): """ super() self._path = Path(get_dict_element("filename", dataset)).resolve() @@ -54,13 +54,12 @@ def _get_original_dataset(self) -> _xr.Dataset: raise NotImplementedError(message) try: - dataset: _xr.Dataset = _xr.open_dataset(self._path, - mask_and_scale=True) + dataset: _xr.Dataset = _xr.open_dataset(self._path, mask_and_scale=True) # mask_and_scale argument is needed to prevent inclusion of NaN's # in dataset for missing values. This inclusion converts integers # to floats - except OSError as exc: + except ValueError as exc: msg = "ERROR: Cannot open input .nc file -- " + str(self._path) - raise OSError(msg) from exc + raise ValueError(msg) from exc return dataset diff --git a/decoimpact/data/entities/model_data_builder.py b/decoimpact/data/entities/model_data_builder.py index 003853f9..07b36638 100644 --- a/decoimpact/data/entities/model_data_builder.py +++ b/decoimpact/data/entities/model_data_builder.py @@ -19,16 +19,12 @@ class ModelDataBuilder: read from the input file to Rule and DatasetData objects)""" def __init__(self) -> None: - """""" + """Create ModelDataBuilder""" self._rule_parsers = list(rule_parsers()) def parse_yaml_data(self, contents: dict[Any, Any]) -> IModelData: - """_summary_ - Args: - contents (dict[Any, Any]): _description_ - Returns: - IModelData: _description_ - """ + """Parse the Yaml input file into a data object""" + print('contennts', contents) datasets = list(self._parse_datasets(contents)) rules = list(self._parse_rules(contents)) diff --git a/decoimpact/data/entities/multiply_rule_data.py b/decoimpact/data/entities/multiply_rule_data.py index 9e421783..7d8cf329 100644 --- a/decoimpact/data/entities/multiply_rule_data.py +++ b/decoimpact/data/entities/multiply_rule_data.py @@ -20,9 +20,10 @@ def __init__( name: str, multipliers: List[float], input_variable: str, - output_variable: str, + output_variable: str = "output", + description: str = "" ): - super().__init__(name, output_variable) + super().__init__(name, output_variable, description) self._input_variable = input_variable self._multipliers = multipliers diff --git a/decoimpact/data/entities/rule_data.py b/decoimpact/data/entities/rule_data.py index 02799d18..d7f92387 100644 --- a/decoimpact/data/entities/rule_data.py +++ b/decoimpact/data/entities/rule_data.py @@ -13,7 +13,9 @@ class RuleData(IRuleData): """Class for storing rule information""" - def __init__(self, name: str, output_variable: str): + def __init__( + self, name: str, output_variable: str = "output", description: str = "" + ): """Create RuleData based on provided info dictionary Args: @@ -21,6 +23,7 @@ def __init__(self, name: str, output_variable: str): """ super() self._name = name + self._description = description self._output_variable = output_variable @property @@ -28,7 +31,13 @@ def name(self) -> str: """Name to the rule""" return self._name + @property + def description(self) -> str: + """Description of the rule""" + return self._description + @property def output_variable(self) -> str: """Data of the rule data""" return self._output_variable + diff --git a/decoimpact/data/parsers/parser_multiply_rule.py b/decoimpact/data/parsers/parser_multiply_rule.py index 235d2288..eb0f97dd 100644 --- a/decoimpact/data/parsers/parser_multiply_rule.py +++ b/decoimpact/data/parsers/parser_multiply_rule.py @@ -2,7 +2,7 @@ Module for ParserMultiplyRule class Classes: - MultiplyRuleParser + ParserMultiplyRule """ from typing import Any, Dict @@ -32,6 +32,11 @@ def parse_dict(self, dictionary: Dict[str, Any]) -> IRuleData: name = get_dict_element("name", dictionary) input_variable_name = get_dict_element("input_variable", dictionary) multipliers = get_dict_element("multipliers", dictionary) + + if not all(isinstance(m, (int, float)) for m in multipliers): + message = f"""Multipliers should be a list of floats, \ + received: {multipliers}""" + raise ValueError(message) output_variable_name = get_dict_element("output_variable", dictionary) return MultiplyRuleData( diff --git a/tests/business/entities/rules/test_multiply_rule.py b/tests/business/entities/rules/test_multiply_rule.py index 20007fc1..57ffc6e8 100644 --- a/tests/business/entities/rules/test_multiply_rule.py +++ b/tests/business/entities/rules/test_multiply_rule.py @@ -26,7 +26,7 @@ def test_execute_value_array_multiplied_by_multipliers(): """Test setting input_variable_names of a RuleBase""" # Arrange & Act - rule = MultiplyRule("test", ["foo"], [0.5, 4.0]) + rule = MultiplyRule("test", ["foo"], [0.5, 4.0], "description") data = [1, 2, 3, 4] value_array = _xr.DataArray(data) multiplied_array = rule.execute(value_array) diff --git a/tests/business/entities/test_rule_based_model.py b/tests/business/entities/test_rule_based_model.py index bb40ee10..2a0f6ad3 100644 --- a/tests/business/entities/test_rule_based_model.py +++ b/tests/business/entities/test_rule_based_model.py @@ -31,6 +31,19 @@ def test_create_rule_based_model_with_defaults(): assert model.status == ModelStatus.CREATED +def test_status_setter(): + # Arrange + rule = Mock(IRule) + dataset = Mock(IDatasetData) + + # Act + model = RuleBasedModel([dataset], [rule]) + + assert model.status == ModelStatus.CREATED + model.status = ModelStatus.EXECUTED + assert model.status == ModelStatus.EXECUTED + + def test_validation_of_rule_based_model(): """Test if the model correctly validates for required parameters (datasets, rules) diff --git a/tests/business/workflow/test_model_factory.py b/tests/business/workflow/test_model_factory.py index 1d2339bd..e6ba6706 100644 --- a/tests/business/workflow/test_model_factory.py +++ b/tests/business/workflow/test_model_factory.py @@ -74,7 +74,5 @@ def test_create_rule_based_model_with_non_supported_rule(): exception_raised = exc_info.value # Assert - expected_message = "The rule type of rule 'test' is currently "\ - "not implemented" + expected_message = "The rule type of rule 'test' is currently " "not implemented" assert exception_raised.args[0] == expected_message - diff --git a/tests/data/entities/test_dataset_data.py b/tests/data/entities/test_dataset_data.py index 06e64244..fec4b0a8 100644 --- a/tests/data/entities/test_dataset_data.py +++ b/tests/data/entities/test_dataset_data.py @@ -2,6 +2,8 @@ Tests for DatasetData class """ +from pathlib import Path + import pytest import xarray as _xr @@ -86,3 +88,27 @@ def test_dataset_data_get_input_dataset_should_check_if_extension_is_correct(): assert exception_raised.args[0].endswith( "Currently only UGrid (NetCDF) files are supported." ) + + +def test_dataset_data_get_input_dataset_should_not_read_incorrect_file(): + """When calling get_input_dataset on a dataset should + read the specified IDatasetData.path. If the file is not correct (not + readable), raise OSError. + """ + + # Arrange + path = get_test_data_path() + "/FlowFM_net_incorrect.nc" + data_dict = {"filename": path, "variable_mapping": {"test": "test_new"}} + data = DatasetData(data_dict) + + # Act + with pytest.raises(ValueError) as exc_info: + data.get_input_dataset() + + exception_raised = exc_info.value + # Assert + + path = Path(path).resolve() + assert exception_raised.args[0].endswith( + f"ERROR: Cannot open input .nc file -- " + str(path) + ) diff --git a/tests/data/entities/test_dataset_data_data/FlowFM_net_incorrect.nc b/tests/data/entities/test_dataset_data_data/FlowFM_net_incorrect.nc new file mode 100644 index 00000000..e8b5ceab --- /dev/null +++ b/tests/data/entities/test_dataset_data_data/FlowFM_net_incorrect.nc @@ -0,0 +1 @@ +test incorrect nc diff --git a/tests/data/entities/test_model_data_builder.py b/tests/data/entities/test_model_data_builder.py new file mode 100644 index 00000000..ae789b9f --- /dev/null +++ b/tests/data/entities/test_model_data_builder.py @@ -0,0 +1,56 @@ +""" +Tests for ModelDataBuilder class +""" + +import pytest + +from decoimpact.data.api.i_model_data import IModelData +from decoimpact.data.entities.model_data_builder import ModelDataBuilder + +contents = dict( + { + "input-data": [ + {"dataset": {"filename": "test", "variabel_mapping": {"foo": "bar"}}} + ], + "rules": [ + { + "multiply_rule": { + "name": "testrule", + "description": "testdescription", + "multipliers": [1, 2.0], + "input_variable": "testin", + "output_variable": "testout", + } + } + ], + } +) + + +def test_model_data_builder_parse_dict_to_model_data(): + """The ModelDataBuilder should parse the provided dictionary + to a IModelData object""" + + # Act + data = ModelDataBuilder() + + parsed_data = data.parse_yaml_data(contents) + assert isinstance(parsed_data, IModelData) + + +def test_model_data_builder_gives_error_when_rule_not_defined(): + """The ModelDataBuilder should parse the provided dictionary + to a IModelData object""" + + # Act + data = ModelDataBuilder() + contents["rules"] = [{"wrong_rule": "test"}] + + with pytest.raises(KeyError) as exc_info: + data.parse_yaml_data(contents) + + exception_raised = exc_info.value + + # Assert + exc = exception_raised.args[0] + assert exc.endswith(f"No parser for wrong_rule") diff --git a/tests/data/entities/test_multiply_rule_data.py b/tests/data/entities/test_multiply_rule_data.py new file mode 100644 index 00000000..74150637 --- /dev/null +++ b/tests/data/entities/test_multiply_rule_data.py @@ -0,0 +1,20 @@ +""" +Tests for MultiplyRuleData class +""" + +from decoimpact.data.api.i_rule_data import IRuleData +from decoimpact.data.entities.multiply_rule_data import MultiplyRuleData + + +def test_multiply_rule_data_creation_logic(): + """The MultiplyRuleData should parse the provided dictionary + to correctly initialize itself during creation""" + + # Act + data = MultiplyRuleData("test_name", [1.0, 2.0], "input", "output", "description") + + # Assert + + assert isinstance(data, IRuleData) + assert data.input_variable == "input" + assert data.multipliers == [1.0, 2.0] diff --git a/tests/data/entities/test_rule_data.py b/tests/data/entities/test_rule_data.py index 5a914405..2d6ff65d 100644 --- a/tests/data/entities/test_rule_data.py +++ b/tests/data/entities/test_rule_data.py @@ -11,10 +11,11 @@ def test_rule_data_creation_logic(): to correctly initialize itself during creation""" # Act - data = RuleData("test_name", "foo") + data = RuleData("test_name", output_variable="foo") # Assert assert isinstance(data, IRuleData) assert data.name == "test_name" + assert data.description == "" assert data.output_variable == "foo" diff --git a/tests/data/entities/test_yaml_model_data.py b/tests/data/entities/test_yaml_model_data.py index 2bf7ba01..a1c44cbf 100644 --- a/tests/data/entities/test_yaml_model_data.py +++ b/tests/data/entities/test_yaml_model_data.py @@ -3,7 +3,13 @@ """ +from unittest.mock import Mock + +from decoimpact.data.api.i_dataset import IDatasetData from decoimpact.data.api.i_model_data import IModelData +from decoimpact.data.api.i_rule_data import IRuleData +from decoimpact.data.entities.dataset_data import DatasetData +from decoimpact.data.entities.multiply_rule_data import MultiplyRuleData from decoimpact.data.entities.yaml_model_data import YamlModelData @@ -12,8 +18,8 @@ def test_yaml_model_data_default_settings_and_type(): interface and gives the right default settings""" # Arrange - datasets = [] - rules = [] + datasets = [Mock(DatasetData)] + rules = [Mock(MultiplyRuleData)] # Act model_data = YamlModelData("Model 1", datasets, rules) @@ -24,3 +30,7 @@ def test_yaml_model_data_default_settings_and_type(): assert isinstance(model_data, IModelData) assert model_data.name == "Model 1" + assert model_data.datasets == datasets + assert isinstance(model_data.datasets[0], IDatasetData) + assert model_data.rules == rules + assert isinstance(model_data.rules[0], IRuleData) diff --git a/tests/data/parsers/test_parser_multiply_rule.py b/tests/data/parsers/test_parser_multiply_rule.py new file mode 100644 index 00000000..69b93440 --- /dev/null +++ b/tests/data/parsers/test_parser_multiply_rule.py @@ -0,0 +1,90 @@ +""" +Tests for ParserMultiplyRule class +""" + +import pytest + +from decoimpact.data.api.i_rule_data import IRuleData +from decoimpact.data.parsers.i_parser_rule_base import IParserRuleBase +from decoimpact.data.parsers.parser_multiply_rule import ParserMultiplyRule + + +def test_parser_multiply_rule_creation_logic(): + """The ParserMultiplyRule should parse the provided dictionary + to correctly initialize itself during creation""" + + # Act + data = ParserMultiplyRule() + + # Assert + + assert isinstance(data, IParserRuleBase) + assert data.rule_type_name == "multiply_rule" + + +def test_parse_dict_to_rule_data_logic(): + """Test if a correct dictionary is parsed into a RuleData object""" + # Arrange + contents = dict( + { + "name": "testname", + "input_variable": "input", + "multipliers": [0.0, 1.0], + "output_variable": "output", + } + ) + + # Act + data = ParserMultiplyRule() + parsed_dict = data.parse_dict(contents) + + assert isinstance(parsed_dict, IRuleData) + + +def test_parse_wrong_dict_to_rule_data_logic(): + """Test if an incorrect dictionary is not parsed""" + # Arrange + contents = dict( + { + "name": "testname", + "input_variable": "input", + "output_variable": "output", + } + ) + + # Act + data = ParserMultiplyRule() + + with pytest.raises(AttributeError) as exc_info: + data.parse_dict(contents) + + exception_raised = exc_info.value + + # Assert + expected_message = "Missing element multipliers" + assert exception_raised.args[0] == expected_message + + +def test_parse_multipliers_type(): + """Test if an incorrect dictionary is not parsed""" + # Arrange + contents = dict( + { + "name": "testname", + "input_variable": "input", + "multipliers": ["a", "b", 2], + "output_variable": "output", + } + ) + + # Act + data = ParserMultiplyRule() + with pytest.raises(ValueError) as exc_info: + data.parse_dict(contents) + + exception_raised = exc_info.value + + # Assert + expected_message = "Multipliers should be a list of floats, \ + received: ['a', 'b', 2]" + assert exception_raised.args[0] == expected_message