diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ecf282fb5..6d23d4f1d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -26,6 +26,8 @@ jobs: - name: Install dependencies run: | pip install black + - name: Check black version + run: black --version - name: Run black run: black --check . diff --git a/flood_adapt/integrator/sfincs_adapter.py b/flood_adapt/integrator/sfincs_adapter.py index edfd885e8..13006c8a2 100644 --- a/flood_adapt/integrator/sfincs_adapter.py +++ b/flood_adapt/integrator/sfincs_adapter.py @@ -346,16 +346,9 @@ def add_green_infrastructure( # Make sure no multipolygons are there gdf_green_infra = gdf_green_infra.explode() - # Determine volume capacity of green infrastructure - if green_infrastructure.height.value != 0.0: - height = ( - green_infrastructure.height.convert(UnitTypesLength("meters")) - * green_infrastructure.percent_area - ) - volume = None - elif green_infrastructure.volume.value != 0.0: - height = None - volume = green_infrastructure.volume.convert(UnitTypesVolume("m3")) + # Volume is always already calculated and is converted to m3 for SFINCS + height = None + volume = green_infrastructure.volume.convert(UnitTypesVolume("m3")) # HydroMT function: create storage volume self.sf_model.setup_storage_volume( diff --git a/flood_adapt/object_model/hazard/measure/green_infrastructure.py b/flood_adapt/object_model/hazard/measure/green_infrastructure.py index ca882897a..66752f573 100644 --- a/flood_adapt/object_model/hazard/measure/green_infrastructure.py +++ b/flood_adapt/object_model/hazard/measure/green_infrastructure.py @@ -15,7 +15,10 @@ IGreenInfrastructure, ) from flood_adapt.object_model.interface.site import ISite -from flood_adapt.object_model.io.unitfulvalue import UnitfulArea, UnitfulLength +from flood_adapt.object_model.io.unitfulvalue import ( + UnitfulArea, + UnitfulHeight, +) class GreenInfrastructure(HazardMeasure, IGreenInfrastructure): @@ -55,7 +58,7 @@ def save(self, filepath: Union[str, os.PathLike]): @staticmethod def calculate_volume( area: UnitfulArea, - height: UnitfulLength = UnitfulLength(value=0.0, units="meters"), + height: UnitfulHeight, percent_area: float = 100.0, ) -> float: """Determine volume from area of the polygon and infiltration height @@ -64,8 +67,8 @@ def calculate_volume( ---------- area : UnitfulArea Area of polygon with units (calculated using calculate_polygon_area) - height : UnitfulLength, optional - Water height with units, by default 0.0 + height : UnitfulHeight + Water height with units percent_area : float, optional Percentage area covered by green infrastructure [%], by default 100.0 diff --git a/flood_adapt/object_model/interface/measures.py b/flood_adapt/object_model/interface/measures.py index 98f2e6d03..8f430febf 100644 --- a/flood_adapt/object_model/interface/measures.py +++ b/flood_adapt/object_model/interface/measures.py @@ -3,15 +3,14 @@ from enum import Enum from typing import Any, Optional, Union -from pydantic import BaseModel, validator +from pydantic import BaseModel, Field, field_validator, model_validator, validator from flood_adapt.object_model.io.unitfulvalue import ( UnitfulDischarge, + UnitfulHeight, UnitfulLength, UnitfulLengthRefValue, UnitfulVolume, - UnitTypesLength, - UnitTypesVolume, ) @@ -50,7 +49,7 @@ class SelectionType(str, Enum): class MeasureModel(BaseModel): """BaseModel describing the expected variables and data types of attributes common to all measures""" - name: str + name: str = Field(..., min_length=1) description: Optional[str] = "" type: Union[HazardType, ImpactType] @@ -59,8 +58,27 @@ class HazardMeasureModel(MeasureModel): """BaseModel describing the expected variables and data types of attributes common to all impact measures""" type: HazardType - polygon_file: Optional[str] = None selection_type: SelectionType + polygon_file: Optional[str] = None + + @field_validator("polygon_file") + @classmethod + def validate_polygon_file(cls, v: Optional[str]) -> Optional[str]: + if len(v) == 0: + raise ValueError("Polygon file path cannot be empty") + return v + + @model_validator(mode="after") + def validate_selection_type(self) -> "HazardMeasureModel": + if ( + self.selection_type + not in [SelectionType.aggregation_area, SelectionType.all] + and self.polygon_file is None + ): + raise ValueError( + "If `selection_type` is not 'aggregation_area' or 'all', then `polygon_file` needs to be set." + ) + return self class ImpactMeasureModel(MeasureModel): @@ -136,23 +154,55 @@ class PumpModel(HazardMeasureModel): class GreenInfrastructureModel(HazardMeasureModel): """BaseModel describing the expected variables and data types of the "green infrastructure" hazard measure""" - volume: UnitfulVolume = UnitfulVolume(value=0.0, units=UnitTypesVolume.m3) - height: UnitfulLength = UnitfulLength(value=0.0, units=UnitTypesLength.meters) + volume: UnitfulVolume + height: Optional[UnitfulHeight] = None aggregation_area_type: Optional[str] = None aggregation_area_name: Optional[str] = None - percent_area: float = 100 - - @validator("volume") - def validate_volume(cls, volume: UnitfulVolume, values: Any) -> UnitfulVolume: - if volume.value <= 0: - raise ValueError("Volume cannot be zero or negative") - return volume - - @validator("height") - def validate_height(cls, height: UnitfulLength, values: Any) -> UnitfulLength: - if height.value <= 0: - raise ValueError("Height cannot be zero or negative") - return height + percent_area: Optional[float] = Field(None, ge=0, le=100) + + @model_validator(mode="after") + def validate_hazard_type_values(self) -> "GreenInfrastructureModel": + if self.type == HazardType.total_storage: + if self.height is not None or self.percent_area is not None: + raise ValueError( + "Height and percent_area cannot be set for total storage type measures" + ) + return self + elif self.type == HazardType.water_square: + if self.percent_area is not None: + raise ValueError( + "Percentage_area cannot be set for water square type measures" + ) + elif not isinstance(self.height, UnitfulHeight): + raise ValueError( + "Height needs to be set for water square type measures" + ) + return self + elif self.type == HazardType.greening: + if not isinstance(self.height, UnitfulHeight) or not isinstance( + self.percent_area, float + ): + raise ValueError( + "Height and percent_area needs to be set for greening type measures" + ) + else: + raise ValueError( + "Type must be one of 'water_square', 'greening', or 'total_storage'" + ) + return self + + @model_validator(mode="after") + def validate_selection_type_values(self) -> "GreenInfrastructureModel": + if self.selection_type == SelectionType.aggregation_area: + if self.aggregation_area_name is None: + raise ValueError( + "If `selection_type` is 'aggregation_area', then `aggregation_area_name` needs to be set." + ) + if self.aggregation_area_type is None: + raise ValueError( + "If `selection_type` is 'aggregation_area', then `aggregation_area_type` needs to be set." + ) + return self class IMeasure(ABC): diff --git a/flood_adapt/object_model/io/unitfulvalue.py b/flood_adapt/object_model/io/unitfulvalue.py index 9b71e1bbc..ab2c594c6 100644 --- a/flood_adapt/object_model/io/unitfulvalue.py +++ b/flood_adapt/object_model/io/unitfulvalue.py @@ -1,6 +1,6 @@ from enum import Enum -from pydantic import BaseModel +from pydantic import BaseModel, Field, root_validator class UnitTypesLength(str, Enum): @@ -100,15 +100,27 @@ def convert(self, new_units: UnitTypesLength) -> float: new_conversion = 3.28084 elif new_units == "inch": new_conversion = 1.0 / 0.0254 - elif self.units == "miles": + elif new_units == "miles": new_conversion = 1.0 / 1609.344 else: ValueError("Invalid length units") return conversion * new_conversion * self.value +class UnitfulHeight(UnitfulLength): + """A special type of length that is always positive and non-zero. Used for heights.""" + + value: float = Field(..., gt=0) + + @root_validator(pre=True) + def convert_length_to_height(cls, obj): + if isinstance(obj, UnitfulLength): + return UnitfulHeight(value=obj.value, units=obj.units) + return obj + + class UnitfulArea(ValueUnitPair): - value: float + value: float = Field(..., gt=0) units: UnitTypesArea def convert(self, new_units: UnitTypesArea) -> float: @@ -127,7 +139,7 @@ def convert(self, new_units: UnitTypesArea) -> float: # first, convert to meters if self.units == "cm2": conversion = 1.0 / 10000 # meters - if self.units == "mm2": + elif self.units == "mm2": conversion = 1.0 / 1000000 # meters elif self.units == "m2": conversion = 1.0 # meters @@ -139,7 +151,7 @@ def convert(self, new_units: UnitTypesArea) -> float: # second, convert to new units if new_units == "cm2": new_conversion = 10000.0 - if new_units == "mm2": + elif new_units == "mm2": new_conversion = 1000000.0 elif new_units == "m2": new_conversion = 1.0 @@ -267,7 +279,7 @@ def convert(self, new_units: UnitTypesIntensity) -> float: class UnitfulVolume(ValueUnitPair): - value: float + value: float = Field(..., gt=0) units: UnitTypesVolume def convert(self, new_units: UnitTypesVolume) -> float: diff --git a/tests/test_object_model/interface/test_measures.py b/tests/test_object_model/interface/test_measures.py new file mode 100644 index 000000000..78f9caa9a --- /dev/null +++ b/tests/test_object_model/interface/test_measures.py @@ -0,0 +1,475 @@ +import pytest + +from flood_adapt.object_model.interface.measures import ( + GreenInfrastructureModel, + HazardMeasureModel, + HazardType, + MeasureModel, + SelectionType, +) +from flood_adapt.object_model.io.unitfulvalue import ( + UnitfulHeight, + UnitfulLength, + UnitfulVolume, + UnitTypesLength, + UnitTypesVolume, +) + + +class TestMeasureModel: + def test_measure_model_correct_input(self): + # Arrange + measure = MeasureModel( + name="test_measure", + description="test description", + type=HazardType.floodwall, + ) + + # Assert + assert measure.name == "test_measure" + assert measure.description == "test description" + assert measure.type == "floodwall" + + def test_measure_model_no_description(self): + # Arrange + measure = MeasureModel(name="test_measure", type=HazardType.floodwall) + + # Assert + assert measure.name == "test_measure" + assert measure.description == "" + assert measure.type == "floodwall" + + def test_measure_model_no_name(self): + # Arrange + with pytest.raises(ValueError) as excinfo: + MeasureModel(type=HazardType.floodwall) + + # Assert + assert "validation error for MeasureModel\nname\n Field required" in str( + excinfo.value + ) + + def test_measure_model_invalid_name(self): + # Arrange + with pytest.raises(ValueError) as excinfo: + MeasureModel(name="", type=HazardType.floodwall) + + # Assert + assert ( + "validation error for MeasureModel\nname\n String should have at least 1 character " + in str(excinfo.value) + ) + + def test_measure_model_invalid_type(self): + # Arrange + with pytest.raises(ValueError) as excinfo: + MeasureModel( + name="test_measure", description="test description", type="invalid_type" + ) + + # Assert + assert "validation errors for MeasureModel\ntype" in str(excinfo.value) + assert "[HazardType]]]\n Input should be " in str(excinfo.value) + assert "[ImpactType]]]\n Input should be " in str(excinfo.value) + + +class TestHazardMeasureModel: + def test_hazard_measure_model_correct_input(self): + # Arrange + hazard_measure = HazardMeasureModel( + name="test_hazard_measure", + description="test description", + type=HazardType.floodwall, + polygon_file="test_polygon_file", + selection_type=SelectionType.aggregation_area, + ) + + # Assert + assert hazard_measure.name == "test_hazard_measure" + assert hazard_measure.description == "test description" + assert hazard_measure.type == "floodwall" + assert hazard_measure.polygon_file == "test_polygon_file" + assert hazard_measure.selection_type == "aggregation_area" + + def test_hazard_measure_model_no_polygon_file_aggregation_area(self): + # Arrange + hazard_measure = HazardMeasureModel( + name="test_hazard_measure", + description="test description", + type=HazardType.floodwall, + selection_type=SelectionType.aggregation_area, + ) + + # Assert + assert hazard_measure.name == "test_hazard_measure" + assert hazard_measure.description == "test description" + assert hazard_measure.type == "floodwall" + assert hazard_measure.polygon_file is None + assert hazard_measure.selection_type == "aggregation_area" + + def test_hazard_measure_model_no_polygon_file_polygon_input(self): + # Arrange + with pytest.raises(ValueError) as excinfo: + HazardMeasureModel( + name="test_hazard_measure", + description="test description", + type=HazardType.floodwall, + selection_type=SelectionType.polygon, + ) + + # Assert + assert "`polygon_file` needs to be set" in str(excinfo.value) + + def test_hazard_measure_model_invalid_type(self): + # Arrange + with pytest.raises(ValueError) as excinfo: + HazardMeasureModel( + name="test_hazard_measure", + description="test description", + type="invalid_type", + polygon_file="test_polygon_file", + selection_type=SelectionType.aggregation_area, + ) + + # Assert + assert "HazardMeasureModel\ntype\n Input should be " in str(excinfo.value) + + def test_hazard_measure_model_invalid_selection_type(self): + # Arrange + with pytest.raises(ValueError) as excinfo: + HazardMeasureModel( + name="test_hazard_measure", + description="test description", + type=HazardType.floodwall, + polygon_file="test_polygon_file", + selection_type="invalid_selection_type", + ) + + # Assert + assert "HazardMeasureModel\nselection_type\n Input should be " in str( + excinfo.value + ) + + +class TestGreenInfrastructureModel: + def test_green_infrastructure_model_correct_aggregation_area_greening_input(self): + # Arrange + green_infrastructure = GreenInfrastructureModel( + name="test_green_infrastructure", + description="test description", + type=HazardType.greening, + polygon_file="test_polygon_file", + selection_type=SelectionType.aggregation_area, + aggregation_area_name="test_aggregation_area_name", + aggregation_area_type="test_aggregation_area_type", + volume=UnitfulVolume(value=1, units=UnitTypesVolume.m3), + height=UnitfulHeight(value=1, units=UnitTypesLength.meters), + percent_area=100.0, + ) + + # Assert + assert green_infrastructure.name == "test_green_infrastructure" + assert green_infrastructure.description == "test description" + assert green_infrastructure.type == "greening" + assert green_infrastructure.polygon_file == "test_polygon_file" + assert green_infrastructure.selection_type == "aggregation_area" + assert ( + green_infrastructure.aggregation_area_name == "test_aggregation_area_name" + ) + assert ( + green_infrastructure.aggregation_area_type == "test_aggregation_area_type" + ) + assert green_infrastructure.volume == UnitfulVolume( + value=1, units=UnitTypesVolume.m3 + ) + assert green_infrastructure.height == UnitfulHeight( + value=1, units=UnitTypesLength.meters + ) + assert green_infrastructure.percent_area == 100.0 + + def test_green_infrastructure_model_correct_total_storage_polygon_input(self): + # Arrange + green_infrastructure = GreenInfrastructureModel( + name="test_green_infrastructure", + description="test description", + type=HazardType.total_storage, + polygon_file="test_polygon_file", + selection_type=SelectionType.polygon, + volume=UnitfulVolume(value=1, units=UnitTypesVolume.m3), + ) # No height or percent_area needed for total storage + + # Assert + assert green_infrastructure.name == "test_green_infrastructure" + assert green_infrastructure.description == "test description" + assert green_infrastructure.type == "total_storage" + assert green_infrastructure.polygon_file == "test_polygon_file" + assert green_infrastructure.selection_type == "polygon" + assert green_infrastructure.volume == UnitfulVolume( + value=1, units=UnitTypesVolume.m3 + ) + + def test_green_infrastructure_model_correct_water_square_polygon_input(self): + # Arrange + green_infrastructure = GreenInfrastructureModel( + name="test_green_infrastructure", + description="test description", + type=HazardType.water_square, + polygon_file="test_polygon_file", + selection_type=SelectionType.polygon, + volume=UnitfulVolume(value=1, units=UnitTypesVolume.m3), + height=UnitfulHeight(value=1, units=UnitTypesLength.meters), + ) # No percent_area needed for water square + + # Assert + assert green_infrastructure.name == "test_green_infrastructure" + assert green_infrastructure.description == "test description" + assert green_infrastructure.type == "water_square" + assert green_infrastructure.polygon_file == "test_polygon_file" + assert green_infrastructure.selection_type == "polygon" + + def test_green_infrastructure_model_no_aggregation_area_name(self): + # Arrange + with pytest.raises(ValueError) as excinfo: + GreenInfrastructureModel( + name="test_green_infrastructure", + description="test description", + type=HazardType.greening, + polygon_file="test_polygon_file", + selection_type=SelectionType.aggregation_area, + aggregation_area_type="test_aggregation_area_type", + volume=UnitfulVolume(value=1, units=UnitTypesVolume.m3), + height=UnitfulHeight(value=1, units=UnitTypesLength.meters), + percent_area=100.0, + ) + + # Assert + assert ( + "If `selection_type` is 'aggregation_area', then `aggregation_area_name` needs to be set." + in str(excinfo.value) + ) + + def test_green_infrastructure_model_no_aggregation_area_type(self): + # Arrange + with pytest.raises(ValueError) as excinfo: + GreenInfrastructureModel( + name="test_green_infrastructure", + description="test description", + type=HazardType.greening, + polygon_file="test_polygon_file", + selection_type=SelectionType.aggregation_area, + aggregation_area_name="test_aggregation_area_name", + volume=UnitfulVolume(value=1, units=UnitTypesVolume.m3), + height=UnitfulHeight(value=1, units=UnitTypesLength.meters), + percent_area=100.0, + ) + + # Assert + assert ( + "If `selection_type` is 'aggregation_area', then `aggregation_area_type` needs to be set." + in str(excinfo.value) + ) + + def test_green_infrastructure_model_other_measure_type(self): + # Arrange + with pytest.raises(ValueError) as excinfo: + GreenInfrastructureModel( + name="test_green_infrastructure", + description="test description", + type=HazardType.floodwall, + polygon_file="test_polygon_file", + selection_type=SelectionType.aggregation_area, + aggregation_area_name="test_aggregation_area_name", + aggregation_area_type="test_aggregation_area_type", + volume=UnitfulVolume(value=1, units=UnitTypesVolume.m3), + height=UnitfulHeight(value=1, units=UnitTypesLength.meters), + percent_area=100.0, + ) + + # Assert + assert "GreenInfrastructureModel\n Value error, Type must be one of " in str( + excinfo.value + ) + + @pytest.mark.parametrize( + "volume, height, percent_area, error_message", + [ + ( + None, + UnitfulHeight(value=1, units=UnitTypesLength.meters), + 100.0, + "volume\n Input should be a valid dictionary or instance of UnitfulVolume", + ), + ( + UnitfulVolume(value=1, units=UnitTypesVolume.m3), + None, + 100.0, + "Height and percent_area needs to be set for greening type measures", + ), + ( + UnitfulVolume(value=1, units=UnitTypesVolume.m3), + UnitfulLength(value=0, units=UnitTypesLength.meters), + None, + "height.value\n Input should be greater than 0", + ), + ( + UnitfulVolume(value=1, units=UnitTypesVolume.m3), + UnitfulLength(value=-1, units=UnitTypesLength.meters), + None, + "height.value\n Input should be greater than 0", + ), + ( + UnitfulVolume(value=1, units=UnitTypesVolume.m3), + UnitfulHeight(value=1, units=UnitTypesLength.meters), + None, + "Height and percent_area needs to be set for greening type measures", + ), + ( + UnitfulVolume(value=1, units=UnitTypesVolume.m3), + UnitfulHeight(value=1, units=UnitTypesLength.meters), + -1, + "percent_area\n Input should be greater than or equal to 0", + ), + ( + UnitfulVolume(value=1, units=UnitTypesVolume.m3), + UnitfulHeight(value=1, units=UnitTypesLength.meters), + 101, + "percent_area\n Input should be less than or equal to 100", + ), + ], + ids=[ + "volume_none", + "height_none", + "unitfulLength_zero", # You should still be able to set as a unitfull length. However, during the conversion to height, it should trigger the height validator + "unitfulLength_negative", # You should still be able to set as a unitfull length. However, during the conversion to height, it should trigger the height validator + "percent_area_none", + "percent_area_negative", + "percent_area_above_100", + ], + ) + def test_green_infrastructure_model_greening_fails( + self, + volume, + height, + percent_area, + error_message, + ): + # Arrange + with pytest.raises(ValueError) as excinfo: + GreenInfrastructureModel( + name="test_green_infrastructure", + description="test description", + type=HazardType.greening, + polygon_file="test_polygon_file", + selection_type=SelectionType.aggregation_area, + aggregation_area_name="test_aggregation_area_name", + aggregation_area_type="test_aggregation_area_type", + volume=volume, + height=height, + percent_area=percent_area, + ) + + # Assert + assert error_message in str(excinfo.value) + + @pytest.mark.parametrize( + "volume, height, percent_area, error_message", + [ + ( + None, + None, + None, + "volume\n Input should be a valid dictionary or instance of UnitfulVolume", + ), + ( + UnitfulVolume(value=1, units=UnitTypesVolume.m3), + UnitfulHeight(value=1, units=UnitTypesLength.meters), + None, + "Height and percent_area cannot be set for total storage type measures", + ), + ( + UnitfulVolume(value=1, units=UnitTypesVolume.m3), + None, + 100, + "Height and percent_area cannot be set for total storage type measures", + ), + ], + ids=[ + "volume_none", + "height_set", + "percent_area_set", + ], + ) + def test_green_infrastructure_model_total_storage_fails( + self, + volume, + height, + percent_area, + error_message, + ): + # Arrange + with pytest.raises(ValueError) as excinfo: + GreenInfrastructureModel( + name="test_green_infrastructure", + description="test description", + type=HazardType.total_storage, + polygon_file="test_polygon_file", + selection_type=SelectionType.polygon, + volume=volume, + height=height, + percent_area=percent_area, + ) + + # Assert + assert "1 validation error for GreenInfrastructureModel" in str(excinfo.value) + assert error_message in str(excinfo.value) + + @pytest.mark.parametrize( + "volume, height, percent_area, error_message", + [ + ( + None, + UnitfulHeight(value=1, units=UnitTypesLength.meters), + None, + "volume\n Input should be a valid dictionary or instance of UnitfulVolume", + ), + ( + UnitfulVolume(value=1, units=UnitTypesVolume.m3), + None, + None, + "Height needs to be set for water square type measures", + ), + ( + UnitfulVolume(value=1, units=UnitTypesVolume.m3), + UnitfulHeight(value=1, units=UnitTypesLength.meters), + 100, + "Percentage_area cannot be set for water square type measures", + ), + ], + ids=[ + "volume_none", + "height_none", + "percent_area_set", + ], + ) + def test_green_infrastructure_model_water_square_fails( + self, + volume, + height, + percent_area, + error_message, + ): + # Arrange + with pytest.raises(ValueError) as excinfo: + GreenInfrastructureModel( + name="test_green_infrastructure", + description="test description", + type=HazardType.water_square, + polygon_file="test_polygon_file", + selection_type=SelectionType.polygon, + volume=volume, + height=height, + percent_area=percent_area, + ) + + # Assert + assert error_message in str(excinfo.value) diff --git a/tests/test_object_model/interface/test_unitfulvalue.py b/tests/test_object_model/interface/test_unitfulvalue.py new file mode 100644 index 000000000..e32b1f563 --- /dev/null +++ b/tests/test_object_model/interface/test_unitfulvalue.py @@ -0,0 +1,265 @@ +import pytest + +from flood_adapt.object_model.io.unitfulvalue import ( + UnitfulArea, + UnitfulHeight, + UnitfulVolume, + UnitTypesArea, + UnitTypesLength, + UnitTypesVolume, +) + + +class TestUnitfulHeight: + def test_unitfulHeight_convertMToFeet_correct(self): + # Assert + length = UnitfulHeight(value=10, units=UnitTypesLength.meters) + + # Act + converted_length = length.convert(UnitTypesLength.feet) + + # Assert + assert round(converted_length, 4) == 32.8084 + + def test_unitfulHeight_convertFeetToM_correct(self): + # Assert + length = UnitfulHeight(value=10, units=UnitTypesLength.feet) + inverse_length = UnitfulHeight(value=10, units=UnitTypesLength.meters) + + # Act + converted_length = length.convert(UnitTypesLength.meters) + inverse_converted_length = inverse_length.convert(UnitTypesLength.feet) + + # Assert + assert round(converted_length, 4) == 3.048 + assert round(inverse_converted_length, 4) == 32.8084 + + def test_unitfulHeight_convertMToCM_correct(self): + # Assert + length = UnitfulHeight(value=10, units=UnitTypesLength.meters) + + # Act + converted_length = length.convert(UnitTypesLength.centimeters) + + # Assert + assert round(converted_length, 4) == 1000 + + def test_unitfulHeight_convertCMToM_correct(self): + # Assert + length = UnitfulHeight(value=1000, units=UnitTypesLength.centimeters) + + # Act + converted_length = length.convert(UnitTypesLength.meters) + + # Assert + assert round(converted_length, 4) == 10 + + def test_unitfulHeight_convertMToMM_correct(self): + # Assert + length = UnitfulHeight(value=10, units=UnitTypesLength.meters) + + # Act + converted_length = length.convert(UnitTypesLength.millimeters) + + # Assert + assert round(converted_length, 4) == 10000 + + def test_unitfulHeight_convertMMToM_correct(self): + # Assert + length = UnitfulHeight(value=10000, units=UnitTypesLength.millimeters) + + # Act + converted_length = length.convert(UnitTypesLength.meters) + + # Assert + assert round(converted_length, 4) == 10 + + def test_unitfulHeight_convertMToInches_correct(self): + # Assert + length = UnitfulHeight(value=10, units=UnitTypesLength.meters) + + # Act + converted_length = length.convert(UnitTypesLength.inch) + + # Assert + assert round(converted_length, 4) == 393.7008 + + def test_unitfulHeight_convertInchesToM_correct(self): + # Assert + length = UnitfulHeight(value=1000, units=UnitTypesLength.inch) + + # Act + converted_length = length.convert(UnitTypesLength.meters) + + # Assert + assert round(converted_length, 4) == 25.4 + + def test_unitfulHeight_convertMToMiles_correct(self): + # Assert + length = UnitfulHeight(value=10, units=UnitTypesLength.meters) + + # Act + converted_length = length.convert(UnitTypesLength.miles) + + # Assert + assert round(converted_length, 4) == 0.0062 + + def test_unitfulHeight_convertMilesToM_correct(self): + # Assert + length = UnitfulHeight(value=1, units=UnitTypesLength.miles) + + # Act + converted_length = length.convert(UnitTypesLength.meters) + + # Assert + assert round(converted_length, 4) == 1609.344 + + def test_unitfulHeight_setValue_negativeValue(self): + # Assert + with pytest.raises(ValueError) as excinfo: + UnitfulHeight(value=-10, units=UnitTypesLength.meters) + assert "UnitfulHeight\nvalue\n Input should be greater than 0" in str( + excinfo.value + ) + + def test_unitfulHeight_setValue_zeroValue(self): + # Assert + with pytest.raises(ValueError) as excinfo: + UnitfulHeight(value=0, units=UnitTypesLength.meters) + assert "UnitfulHeight\nvalue\n Input should be greater than 0" in str( + excinfo.value + ) + + def test_unitfulHeight_setUnit_invalidUnits(self): + # Assert + with pytest.raises(ValueError) as excinfo: + UnitfulHeight(value=10, units="invalid_units") + assert "UnitfulHeight\nunits\n Input should be " in str(excinfo.value) + + +class TestUnitfulArea: + def test_unitfulArea_convertM2ToCM2_correct(self): + # Assert + area = UnitfulArea(value=10, units=UnitTypesArea.m2) + + # Act + converted_area = area.convert(UnitTypesArea.cm2) + + # Assert + assert round(converted_area, 4) == 100000 + + def test_unitfulArea_convertCM2ToM2_correct(self): + # Assert + area = UnitfulArea(value=100000, units=UnitTypesArea.cm2) + + # Act + converted_area = area.convert(UnitTypesArea.m2) + + # Assert + assert round(converted_area, 4) == 10 + + def test_unitfulArea_convertM2ToMM2_correct(self): + # Assert + area = UnitfulArea(value=10, units=UnitTypesArea.m2) + + # Act + converted_area = area.convert(UnitTypesArea.mm2) + + # Assert + assert round(converted_area, 4) == 10000000 + + def test_unitfulArea_convertMM2ToM2_correct(self): + # Assert + area = UnitfulArea(value=10000000, units=UnitTypesArea.mm2) + + # Act + converted_area = area.convert(UnitTypesArea.m2) + + # Assert + assert round(converted_area, 4) == 10 + + def test_unitfulArea_convertM2ToSF_correct(self): + # Assert + area = UnitfulArea(value=10, units=UnitTypesArea.m2) + + # Act + converted_area = area.convert(UnitTypesArea.sf) + + # Assert + assert round(converted_area, 4) == 107.64 + + def test_unitfulArea_convertSFToM2_correct(self): + # Assert + area = UnitfulArea(value=100, units=UnitTypesArea.sf) + + # Act + converted_area = area.convert(UnitTypesArea.m2) + + # Assert + assert round(converted_area, 4) == 9.2902 + + def test_unitfulArea_setValue_negativeValue(self): + # Assert + with pytest.raises(ValueError) as excinfo: + UnitfulArea(value=-10, units=UnitTypesArea.m2) + assert "UnitfulArea\nvalue\n Input should be greater than 0" in str( + excinfo.value + ) + + def test_unitfulArea_setValue_zeroValue(self): + # Assert + with pytest.raises(ValueError) as excinfo: + UnitfulArea(value=0, units=UnitTypesArea.m2) + assert "UnitfulArea\nvalue\n Input should be greater than 0" in str( + excinfo.value + ) + + def test_unitfulArea_setUnit_invalidUnits(self): + # Assert + with pytest.raises(ValueError) as excinfo: + UnitfulArea(value=10, units="invalid_units") + assert "UnitfulArea\nunits\n Input should be " in str(excinfo.value) + + +class TestUnitfulVolume: + def test_unitfulVolume_convertM3ToCF_correct(self): + # Assert + volume = UnitfulVolume(value=10, units=UnitTypesVolume.m3) + + # Act + converted_volume = volume.convert(UnitTypesVolume.cf) + + # Assert + assert round(converted_volume, 4) == 353.1466 + + def test_unitfulVolume_convertCFToM3_correct(self): + # Assert + volume = UnitfulVolume(value=100, units=UnitTypesVolume.cf) + + # Act + converted_volume = volume.convert(UnitTypesVolume.m3) + + # Assert + assert round(converted_volume, 4) == 2.8317 + + def test_unitfulVolume_setValue_negativeValue(self): + # Assert + with pytest.raises(ValueError) as excinfo: + UnitfulVolume(value=-10, units=UnitTypesVolume.m3) + assert "UnitfulVolume\nvalue\n Input should be greater than 0" in str( + excinfo.value + ) + + def test_unitfulVolume_setValue_zeroValue(self): + # Assert + with pytest.raises(ValueError) as excinfo: + UnitfulVolume(value=0, units=UnitTypesVolume.m3) + assert "UnitfulVolume\nvalue\n Input should be greater than 0" in str( + excinfo.value + ) + + def test_unitfulVolume_setUnit_invalidUnits(self): + # Assert + with pytest.raises(ValueError) as excinfo: + UnitfulVolume(value=10, units="invalid_units") + assert "UnitfulVolume\nunits\n Input should be " in str(excinfo.value)