diff --git a/docs/index.rst b/docs/index.rst index b8bc5a2..f16492c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,5 +1,5 @@ Welcome to BAEC Model Generator SDK documentation! -======================================= +================================================== The `baec` library is created by `CEMS BV `_ . diff --git a/docs/tree/reference.rst b/docs/tree/reference.rst index 70f671c..5d74895 100644 --- a/docs/tree/reference.rst +++ b/docs/tree/reference.rst @@ -60,7 +60,22 @@ Measured Settlement Series :member-order: bysource .. automethod:: __init__ - + +.. _statusMessage: + +Status Message +-------------- + +.. autoclass:: baec.measurements.settlement_rod_measurement.StatusMessage + :members: + :inherited-members: + :member-order: bysource + + .. automethod:: __init__ + +.. autoenum:: baec.measurements.settlement_rod_measurement.StatusMessageLevel + :members: + .. _measurementDevice: Measurement Device diff --git a/src/baec/measurements/io/zbase.py b/src/baec/measurements/io/zbase.py index 1ca66c5..9ced17d 100644 --- a/src/baec/measurements/io/zbase.py +++ b/src/baec/measurements/io/zbase.py @@ -11,7 +11,8 @@ from baec.measurements.measurement_device import MeasurementDevice from baec.measurements.settlement_rod_measurement import ( SettlementRodMeasurement, - SettlementRodMeasurementStatus, + StatusMessage, + StatusMessageLevel, ) from baec.measurements.settlement_rod_measurement_series import ( SettlementRodMeasurementSeries, @@ -19,20 +20,59 @@ from baec.project import Project _STATUS_MAP = { - 0: "ok", - 1: "ok", - 2: "disturbed", - 3: "expired", - 4: "relocated", - 5: "rod_is_extended", - 6: "crooked", - 7: "deselected", - 8: "deselected", - 9: "fictional", - 10: "unknown", + 0: StatusMessage(code=0, description="OK", level=StatusMessageLevel.OK), + 1: StatusMessage(code=1, description="OK", level=StatusMessageLevel.OK), + 2: StatusMessage(code=2, description="Disturbed", level=StatusMessageLevel.WARNING), + 3: StatusMessage(code=3, description="Expired", level=StatusMessageLevel.WARNING), + 4: StatusMessage(code=4, description="Relocated", level=StatusMessageLevel.WARNING), + 5: StatusMessage( + code=5, description="Rod is extended", level=StatusMessageLevel.INFO + ), + 6: StatusMessage(code=6, description="Crooked", level=StatusMessageLevel.WARNING), + 7: StatusMessage( + code=7, description="Deselected", level=StatusMessageLevel.WARNING + ), + 8: StatusMessage( + code=8, description="Deselected", level=StatusMessageLevel.WARNING + ), + 9: StatusMessage(code=9, description="Fictional", level=StatusMessageLevel.WARNING), } +def _zbase_status_to_message(status: int) -> StatusMessage: + """ + Convert a ZBase status code to a StatusMessage object. + If the status code is not recognized, a default StatusMessage object is returned + with level set to StatusMessageLevel.ERROR. + + Parameters + ---------- + status : int + The ZBase status code. + + Returns + ------- + status_message : StatusMessage + The corresponding StatusMessage object. + + Raises + ------ + TypeError + If status is not of type int. + """ + if not isinstance(status, int): + raise TypeError(f"status must be of type `int`, but got {type(status)}.") + + status_message = _STATUS_MAP.get(status) + if status_message is None: + status_message = StatusMessage( + code=status, + description="Unrecognized status code", + level=StatusMessageLevel.ERROR, + ) + return status_message + + def measurements_from_zbase( filepath_or_buffer: str | PathLike[str] | ReadCsvBuffer[bytes] | ReadCsvBuffer[str], project_name: str, @@ -98,6 +138,7 @@ def measurements_from_zbase( # create SettlementRodMeasurement objects measurements = [] for _, row in df.iterrows(): + status = row.get("status") measurements.append( SettlementRodMeasurement( project=Project(id_=id_, name=project_name), @@ -113,9 +154,9 @@ def measurements_from_zbase( rod_length=row.get("rod_top_z", 0) - row.get("rod_bottom_z", 0), rod_bottom_z=row.get("rod_bottom_z", 0), ground_surface_z=row.get("ground_surface_z", 0), - status=SettlementRodMeasurementStatus( - _STATUS_MAP.get(row.get("status", 10), "unknown") - ), + status_messages=[] + if status is None + else [_zbase_status_to_message(status)], ) ) diff --git a/src/baec/measurements/measured_settlement.py b/src/baec/measurements/measured_settlement.py index a9733c4..388a284 100644 --- a/src/baec/measurements/measured_settlement.py +++ b/src/baec/measurements/measured_settlement.py @@ -2,10 +2,12 @@ import datetime from functools import cache, cached_property +from typing import List from baec.measurements.settlement_rod_measurement import ( SettlementRodMeasurement, SettlementRodMeasurementStatus, + StatusMessage, ) from baec.project import Project @@ -29,6 +31,7 @@ def __init__( horizontal_units: str, vertical_units: str, status: SettlementRodMeasurementStatus, + status_messages: List[StatusMessage], ) -> None: """ Initializes a MeasuredSettlement object. @@ -63,6 +66,9 @@ def __init__( status: SettlementRodMeasurementStatus The status of the settlement rod measurement from which the measured settlement is derived. + status_messages: List[StatusMessage] + The list of status messages about the settlement rod measurement from which the + measured settlement is derived. Raises ------ @@ -86,6 +92,7 @@ def __init__( self._set_horizontal_units(horizontal_units) self._set_vertical_units(vertical_units) self._set_status(status) + self._set_status_messages(status_messages) @classmethod def from_settlement_rod_measurement( @@ -156,6 +163,7 @@ def from_settlement_rod_measurement( horizontal_units=measurement.coordinate_reference_systems.horizontal_units, vertical_units=measurement.coordinate_reference_systems.vertical_units, status=measurement.status, + status_messages=measurement.status_messages, ) def _set_project(self, value: Project) -> None: @@ -274,6 +282,21 @@ def _set_status(self, value: SettlementRodMeasurementStatus) -> None: ) self._status = value + def _set_status_messages(self, value: List[StatusMessage]) -> None: + """ + Private setter for status attribute. + """ + if not isinstance(value, list): + raise TypeError( + "Expected 'List[StatusMessage]' type for 'status_messages' attribute." + ) + # Check if the input is a list of StatusMessage objects. + if not all(isinstance(item, StatusMessage) for item in value): + raise TypeError( + "Expected 'List[StatusMessage]' type for 'status_messages' attribute." + ) + self._status_messages = value + @property def project(self) -> Project: """ @@ -364,6 +387,14 @@ def status(self) -> SettlementRodMeasurementStatus: """ return self._status + @property + def status_messages(self) -> List[StatusMessage]: + """ + The list of status messages about the settlement rod measurement from which the + measured settlement is derived. + """ + return self._status_messages + @cache def to_dict(self) -> dict: """ @@ -383,4 +414,5 @@ def to_dict(self) -> dict: "horizontal_units": self.horizontal_units, "vertical_units": self.vertical_units, "status": self.status.value, + "status_messages": "\n".join([m.to_string() for m in self.status_messages]), } diff --git a/src/baec/measurements/measured_settlement_series.py b/src/baec/measurements/measured_settlement_series.py index 843045c..02fb355 100644 --- a/src/baec/measurements/measured_settlement_series.py +++ b/src/baec/measurements/measured_settlement_series.py @@ -139,18 +139,20 @@ def __init__( If the `start_index` is out of range for the series. """ - # Check the types of the input parameters. - if not isinstance(series, SettlementRodMeasurementSeries): - raise TypeError( - "Expected 'SettlementRodMeasurementSeries' type for 'series' parameter." - ) - # set SettlementRodMeasurementSeries - self._series = series + self._set_series(series) # set start of settlement self._set_start_index_or_start_date_time(start_index, start_date_time) + def _set_series(self, value: SettlementRodMeasurementSeries) -> None: + """Private setter for series attribute.""" + if not isinstance(value, SettlementRodMeasurementSeries): + raise TypeError( + "Expected 'SettlementRodMeasurementSeries' type for 'series' attribute." + ) + self._series = value + def _set_start_index_or_start_date_time( self, start_index: int | None = None, @@ -438,7 +440,7 @@ def to_dataframe(self) -> pd.DataFrame: A pandas DataFrame with the measured settlements. The columns of the DataFrame are: project_id, project_name, object_id, start_date_time date_time, days, fill_thickness, settlement, x_displacement, y_displacement - horizontal_units, vertical_units, status + horizontal_units, vertical_units, status, status_messages """ return pd.DataFrame.from_records( [measurement.to_dict() for measurement in self.items] diff --git a/src/baec/measurements/settlement_rod_measurement.py b/src/baec/measurements/settlement_rod_measurement.py index 605a427..1dcda56 100644 --- a/src/baec/measurements/settlement_rod_measurement.py +++ b/src/baec/measurements/settlement_rod_measurement.py @@ -2,7 +2,8 @@ import datetime from enum import Enum -from functools import cache +from functools import cache, cached_property, total_ordering +from typing import List from baec.coordinates import CoordinateReferenceSystems from baec.measurements.measurement_device import MeasurementDevice @@ -12,15 +13,142 @@ class SettlementRodMeasurementStatus(Enum): """Represents the status of a settlement rod measurement.""" - OK = "ok" - DISTURBED = "disturbed" - EXPIRED = "expired" - RELOCATED = "relocated" - ROD_IS_EXTENDED = "rod_is_extended" - CROOKED = "crooked" - DESELECTED = "deselected" - FICTIONAL = "fictional" - UNKNOWN = "unknown" + """If the highest severity level of the status messages is "OK". + This will indicate that the measurement is correct.""" + OK = "OK" + + """If the highest severity level of the status messages is "INFO". + This will indicate that there is still at least one informative comment about the measurement. The measurement is still correct.""" + INFO = "INFO" + + """If the highest severity level of the status messages is "WARNING". + This will indicate that there is still at least one warning about the measurement. The measurement may not be correct or accurate enough.""" + WARNING = "WARNING" + + """If the highest severity level of the status messages is "ERROR". + This will indicate that there is still at least one error about the measurement. The measurement is most probably incorrect.""" + ERROR = "ERROR" + + +@total_ordering +class StatusMessageLevel(Enum): + """Represents the severity level of a status message.""" + + """Measurement is correct.""" + OK = "OK" + + """Measurement has an informative comment. The measurement is still correct.""" + INFO = "INFO" + + """Measurement has a warning. The measurement may not be correct or accurate enough.""" + WARNING = "WARNING" + + """Measurement has an error. The measurement is most probably incorrect.""" + ERROR = "ERROR" + + def __lt__(self, other: object) -> bool: + """ + Compares the order of the status message level. + """ + if isinstance(other, StatusMessageLevel): + return self.order() < other.order() + return False + + def order(self) -> int: + """ + Returns the order of the status message level. + """ + order = { + StatusMessageLevel.OK: 0, + StatusMessageLevel.INFO: 1, + StatusMessageLevel.WARNING: 2, + StatusMessageLevel.ERROR: 3, + } + return order[self] + + +class StatusMessage: + """ + Represents a status message of a single settlement rod measurement. + """ + + def __init__( + self, + code: int, + description: str, + level: StatusMessageLevel, + ): + """ + Initializes a StatusMessage object. + + Parameters + ---------- + code : int + The code of the status message. + description : str + The description of the status message. + level : StatusMessageLevel + The severity level of the status message. + """ + + # Initialize all attributes using private setters. + self._set_code(code) + self._set_description(description) + self._set_level(level) + + def _set_code(self, value: int) -> None: + """ + Private setter for code attribute. + """ + if not isinstance(value, int): + raise TypeError("Expected 'int' type for 'code' attribute.") + self._code = value + + def _set_description(self, value: str) -> None: + """ + Private setter for description attribute. + """ + if not isinstance(value, str): + raise TypeError("Expected 'str' type for 'description' attribute.") + if value == "": + raise ValueError("Empty string not allowed for 'description' attribute.") + self._description = value + + def _set_level(self, value: StatusMessageLevel) -> None: + """ + Private setter for level attribute. + """ + if not isinstance(value, StatusMessageLevel): + raise TypeError("Expected 'StatusMessageLevel' type for 'level' attribute.") + self._level = value + + @cache + def to_string(self) -> str: + """ + Convert the status message to a string. + """ + return f"(code={self.code}, description={self.description}, level={self.level.value})" + + @property + def code(self) -> int: + """ + The code of the status message. + """ + return self._code + + @property + def description(self) -> str: + """ + The description of the status message. + """ + return self._description + + @property + def level(self) -> StatusMessageLevel: + """ + The severity level of the status message. + """ + return self._level class SettlementRodMeasurement: @@ -41,10 +169,9 @@ def __init__( rod_length: float, rod_bottom_z: float, ground_surface_z: float, - status: SettlementRodMeasurementStatus, + status_messages: List[StatusMessage], temperature: float | None = None, voltage: float | None = None, - comment: str = "", ) -> None: """ Initializes a SettlementRodMeasurement object. @@ -85,14 +212,12 @@ def __init__( The Z-coordinate of the ground surface. It is in principle the top of the fill, if present. Units and datum according to the `coordinate_reference_systems`. - status: SettlementRodMeasurementStatus - The status of the measurement. + status_messages: List[StatusMessage] + The list of status messages about the measurement. temperature : float or None, optional The temperature at the time of measurement in [°C], or None if unknown (default: None). voltage : float or None, optional The voltage measured in [mV], or None if unknown (default: None). - comment : str, optional - Additional comment about the measurement (default: ""). Raises ------ @@ -115,10 +240,9 @@ def __init__( self._set_rod_length(rod_length) self._set_rod_bottom_z(rod_bottom_z) self._set_ground_surface_z(ground_surface_z) - self._set_status(status) + self._set_status_messages(status_messages) self._set_temperature(temperature) self._set_voltage(voltage) - self._set_comment(comment) def _set_project(self, value: Project) -> None: """ @@ -230,15 +354,20 @@ def _set_ground_surface_z(self, value: float) -> None: raise TypeError("Expected 'float' type for 'ground_surface_z' attribute.") self._ground_surface_z = value - def _set_status(self, value: SettlementRodMeasurementStatus) -> None: + def _set_status_messages(self, value: List[StatusMessage]) -> None: """ Private setter for status attribute. """ - if not isinstance(value, SettlementRodMeasurementStatus): + if not isinstance(value, list): + raise TypeError( + "Expected 'List[StatusMessage]' type for 'status_messages' attribute." + ) + # Check if the input is a list of StatusMessage objects. + if not all(isinstance(item, StatusMessage) for item in value): raise TypeError( - "Expected 'SettlementRodMeasurementStatus' type for 'status' attribute." + "Expected 'List[StatusMessage]' type for 'status_messages' attribute." ) - self._status = value + self._status_messages = value def _set_temperature(self, value: float | None) -> None: """ @@ -266,14 +395,6 @@ def _set_voltage(self, value: float | None) -> None: ) self._voltage = value - def _set_comment(self, value: str) -> None: - """ - Private setter for comment attribute. - """ - if not isinstance(value, str): - raise TypeError("Expected 'str' type for 'comment' attribute.") - self._comment = value - @property def project(self) -> Project: """ @@ -371,11 +492,38 @@ def ground_surface_z(self) -> float: return self._ground_surface_z @property + def status_messages(self) -> List[StatusMessage]: + """ + The list of status messages about the measurement. + """ + return self._status_messages + + @cached_property def status(self) -> SettlementRodMeasurementStatus: """ The status of the measurement. """ - return self._status + + # If no status messages are available, return OK. + if len(self.status_messages) == 0: + return SettlementRodMeasurementStatus.OK + + # Get the highest severity level of the status messages. + highest_level = max([message.level for message in self.status_messages]) + + # Return the corresponding status. + if highest_level == StatusMessageLevel.OK: + return SettlementRodMeasurementStatus.OK + elif highest_level == StatusMessageLevel.INFO: + return SettlementRodMeasurementStatus.INFO + elif highest_level == StatusMessageLevel.WARNING: + return SettlementRodMeasurementStatus.WARNING + elif highest_level == StatusMessageLevel.ERROR: + return SettlementRodMeasurementStatus.ERROR + else: + raise ValueError( + f"No corresponding SettlementRodMeasurementStatus is available for {highest_level}." + ) @property def temperature(self) -> float | None: @@ -391,13 +539,6 @@ def voltage(self) -> float | None: """ return self._voltage - @property - def comment(self) -> str: - """ - Additional comment about the measurement. - """ - return self._comment - @cache def to_dict(self) -> dict: """ @@ -423,7 +564,7 @@ def to_dict(self) -> dict: "rod_bottom_z_uncorrected": self.rod_bottom_z_uncorrected, "ground_surface_z": self.ground_surface_z, "status": self.status.value, + "status_messages": "\n".join([m.to_string() for m in self.status_messages]), "temperature": self.temperature, "voltage": self.voltage, - "comment": self.comment, } diff --git a/src/baec/measurements/settlement_rod_measurement_series.py b/src/baec/measurements/settlement_rod_measurement_series.py index 4916256..2cc7275 100644 --- a/src/baec/measurements/settlement_rod_measurement_series.py +++ b/src/baec/measurements/settlement_rod_measurement_series.py @@ -149,8 +149,8 @@ def to_dataframe(self) -> pd.DataFrame: project_id, project_name, device_id, device_qr_code, object_id, coordinate_horizontal_epsg_code, coordinate_vertical_epsg_code, date_time, rod_top_x, rod_top_y, rod_top_z, rod_length, rod_bottom_z - rod_bottom_z_uncorrected, ground_surface_z, status, temperature, - voltage, comment. + rod_bottom_z_uncorrected, ground_surface_z, status, status_messages, temperature, + voltage. """ return pd.DataFrame.from_records( [measurement.to_dict() for measurement in self.measurements] diff --git a/tests/measurements/conftest.py b/tests/measurements/conftest.py index ba198c9..951e945 100644 --- a/tests/measurements/conftest.py +++ b/tests/measurements/conftest.py @@ -10,6 +10,8 @@ from baec.measurements.settlement_rod_measurement import ( SettlementRodMeasurement, SettlementRodMeasurementStatus, + StatusMessage, + StatusMessageLevel, ) from baec.measurements.settlement_rod_measurement_series import ( SettlementRodMeasurementSeries, @@ -31,10 +33,11 @@ def valid_settlement_rod_measurement_input() -> dict: rod_length=2.0, rod_bottom_z=-1.193, ground_surface_z=0.419, - status=SettlementRodMeasurementStatus.OK, + status_messages=[ + StatusMessage(code=0, description="OK", level=StatusMessageLevel.OK), + ], temperature=12.0, voltage=4232, - comment="No comment", ) @@ -58,10 +61,11 @@ def example_settlement_rod_measurements() -> List[SettlementRodMeasurement]: rod_length = 2.0 rod_bottom_z_start = -1.193 ground_surface_z_start = 0.419 - status = SettlementRodMeasurementStatus.OK + status_messages = [ + StatusMessage(code=0, description="OK", level=StatusMessageLevel.OK), + ] temperature = 12.0 voltage = 4232 - comment = "No comment" measurements = [] for i in range(10): @@ -84,10 +88,9 @@ def example_settlement_rod_measurements() -> List[SettlementRodMeasurement]: rod_length=rod_length, rod_bottom_z=rod_bottom_z, ground_surface_z=ground_surface_z, - status=status, + status_messages=status_messages, temperature=temperature, voltage=voltage, - comment=comment, ) ) @@ -117,6 +120,9 @@ def valid_measured_settlement_input() -> dict: x_displacement=0.25, y_displacement=0.75, status=SettlementRodMeasurementStatus.OK, + status_messages=[ + StatusMessage(code=0, description="OK", level=StatusMessageLevel.OK), + ], ) diff --git a/tests/measurements/test_measured_settlement.py b/tests/measurements/test_measured_settlement.py index c893578..dc08d95 100644 --- a/tests/measurements/test_measured_settlement.py +++ b/tests/measurements/test_measured_settlement.py @@ -9,6 +9,8 @@ from baec.measurements.settlement_rod_measurement import ( SettlementRodMeasurement, SettlementRodMeasurementStatus, + StatusMessage, + StatusMessageLevel, ) from baec.project import Project @@ -26,6 +28,9 @@ def test_measured_settlement_init_with_valid_input() -> None: x_displacement = 0.25 y_displacement = 0.75 status = SettlementRodMeasurementStatus.OK + status_messages = [ + StatusMessage(code=0, description="OK", level=StatusMessageLevel.OK), + ] measured_settlement = MeasuredSettlement( project=project, @@ -39,6 +44,7 @@ def test_measured_settlement_init_with_valid_input() -> None: x_displacement=x_displacement, y_displacement=y_displacement, status=status, + status_messages=status_messages, ) assert measured_settlement.project == project @@ -52,6 +58,7 @@ def test_measured_settlement_init_with_valid_input() -> None: assert measured_settlement.x_displacement == x_displacement assert measured_settlement.y_displacement == y_displacement assert measured_settlement.status == status + assert measured_settlement.status_messages == status_messages assert measured_settlement.days == pytest.approx(8.51, abs=0.001) @@ -91,10 +98,10 @@ def test_measured_settlement_with_invalid_input( MeasuredSettlement(**valid_measured_settlement_input) -def test_from_settlement_rod_settlement_with_valid_input( +def test_from_settlement_rod_settlement( valid_settlement_rod_measurement: SettlementRodMeasurement, ) -> None: - """Test constructor method from_measured_settlement_rod_measurement with valid input.""" + """Test constructor method from_measured_settlement_rod_measurement.""" zero_measurement = deepcopy(valid_settlement_rod_measurement) measurement = deepcopy(zero_measurement) measurement._date_time = datetime.datetime(2024, 4, 10, 0, 0, 0) @@ -126,7 +133,8 @@ def test_from_settlement_rod_settlement_with_valid_input( measured_settlement.vertical_units == measurement.coordinate_reference_systems.vertical_units ) - assert measured_settlement.status == measured_settlement.status + assert measured_settlement.status == measurement.status + assert measured_settlement.status_messages == measurement.status_messages # Invalid measurement: None with pytest.raises(TypeError, match="measurement"): @@ -189,6 +197,9 @@ def test_measured_settlement_to_dict() -> None: x_displacement = 0.25 y_displacement = 0.75 status = SettlementRodMeasurementStatus.OK + status_messages = [ + StatusMessage(code=0, description="OK", level=StatusMessageLevel.OK), + ] measured_settlement = MeasuredSettlement( project=project, @@ -201,7 +212,8 @@ def test_measured_settlement_to_dict() -> None: settlement=settlement, x_displacement=x_displacement, y_displacement=y_displacement, - status=SettlementRodMeasurementStatus.OK, + status=status, + status_messages=status_messages, ) assert measured_settlement.to_dict() == { @@ -218,4 +230,5 @@ def test_measured_settlement_to_dict() -> None: "horizontal_units": horizontal_units, "vertical_units": vertical_units, "status": status.value, + "status_messages": "(code=0, description=OK, level=OK)", } diff --git a/tests/measurements/test_measured_settlement_series.py b/tests/measurements/test_measured_settlement_series.py index e1433b1..c14de55 100644 --- a/tests/measurements/test_measured_settlement_series.py +++ b/tests/measurements/test_measured_settlement_series.py @@ -1,4 +1,7 @@ +from __future__ import annotations + import datetime +from typing import Type import pytest from matplotlib import pyplot as plt @@ -9,16 +12,36 @@ ) -def test_from_settlement_rod_measurement_series_with_valid_input( +@pytest.mark.parametrize( + "start_index,start_date_time,expected_start_index", + [ + (None, None, 0), # default start index and start date time + (5, None, 5), + (-2, None, -2), + (None, datetime.datetime(2024, 4, 11, 0, 0, 0), 2), + (None, datetime.datetime(2024, 4, 11, 4, 0, 0), 3), + ], +) +def test_measured_settlement_series_init_with_valid_input( example_settlement_rod_measurement_series: SettlementRodMeasurementSeries, + start_index: int, + start_date_time: datetime.datetime, + expected_start_index: int, ) -> None: - """Test constructor method from_settlement_rod_measurement_series with valid input.""" + """Test initialization method of MeasuredSettlementSeries with valid input.""" measurement_series = example_settlement_rod_measurement_series - # Valid input with default start_index and start_date_time. - series = MeasuredSettlementSeries( - series=measurement_series, - ) + # Create series + if start_index is None and start_date_time is None: + series = MeasuredSettlementSeries( + series=measurement_series, + ) + else: + series = MeasuredSettlementSeries( + series=measurement_series, + start_index=start_index, + start_date_time=start_date_time, + ) assert isinstance(series, MeasuredSettlementSeries) assert series.project == measurement_series.project @@ -33,38 +56,8 @@ def test_from_settlement_rod_measurement_series_with_valid_input( ) df = measurement_series.to_dataframe() - idx = 0 # expected start index - assert len(series.items) == len(df.iloc[idx:]) - assert series.start_date_time == measurement_series.measurements[idx].date_time - assert series.date_times == df["date_time"].to_list()[idx:] - assert series.days == list( - [ - (d - series.start_date_time).total_seconds() / 86400.0 - for d in df["date_time"].to_list()[idx:] - ] - ) - assert ( - series.fill_thicknesses - == (df["ground_surface_z"] - df["rod_bottom_z"]).to_list()[idx:] - ) - assert ( - series.settlements - == (df["rod_bottom_z"].iloc[idx] - df["rod_bottom_z"].iloc[idx:]).to_list() - ) - assert ( - series.x_displacements - == (df["rod_top_x"].iloc[idx:] - df["rod_top_x"].iloc[idx]).to_list() - ) - assert ( - series.y_displacements - == (df["rod_top_y"].iloc[idx:] - df["rod_top_y"].iloc[idx]).to_list() - ) - # Valid input using start_index = 5. - series.start_index = 5 - - df = measurement_series.to_dataframe() - idx = 5 # expected start index + idx = expected_start_index # expected start index assert len(series.items) == len(df.iloc[idx:]) assert series.start_date_time == measurement_series.measurements[idx].date_time assert series.date_times == df["date_time"].to_list()[idx:] @@ -91,104 +84,11 @@ def test_from_settlement_rod_measurement_series_with_valid_input( == (df["rod_top_y"].iloc[idx:] - df["rod_top_y"].iloc[idx]).to_list() ) - # Valid input using start_index = -2. - series.start_index = -2 - df = measurement_series.to_dataframe() - idx = -2 # expected start index - assert len(series.items) == len(df.iloc[idx:]) - assert series.start_date_time == measurement_series.measurements[idx].date_time - assert series.date_times == df["date_time"].to_list()[idx:] - assert series.days == list( - [ - (d - series.start_date_time).total_seconds() / 86400.0 - for d in df["date_time"].to_list()[idx:] - ] - ) - assert ( - series.fill_thicknesses - == (df["ground_surface_z"] - df["rod_bottom_z"]).to_list()[idx:] - ) - assert ( - series.settlements - == (df["rod_bottom_z"].iloc[idx] - df["rod_bottom_z"].iloc[idx:]).to_list() - ) - assert ( - series.x_displacements - == (df["rod_top_x"].iloc[idx:] - df["rod_top_x"].iloc[idx]).to_list() - ) - assert ( - series.y_displacements - == (df["rod_top_y"].iloc[idx:] - df["rod_top_y"].iloc[idx]).to_list() - ) - - # Valid input using start_date_time = 2024-04-11 00:00:00. - series.start_date_time = datetime.datetime(2024, 4, 11, 0, 0, 0) - - df = measurement_series.to_dataframe() - idx = 2 # expected start index - assert len(series.items) == len(df.iloc[idx:]) - assert series.start_date_time == datetime.datetime(2024, 4, 11, 0, 0, 0) - assert series.date_times == df["date_time"].to_list()[idx:] - assert series.days == list( - [ - (d - series.start_date_time).total_seconds() / 86400.0 - for d in df["date_time"].to_list()[idx:] - ] - ) - assert ( - series.fill_thicknesses - == (df["ground_surface_z"] - df["rod_bottom_z"]).to_list()[idx:] - ) - assert ( - series.settlements - == (df["rod_bottom_z"].iloc[idx] - df["rod_bottom_z"].iloc[idx:]).to_list() - ) - assert ( - series.x_displacements - == (df["rod_top_x"].iloc[idx:] - df["rod_top_x"].iloc[idx]).to_list() - ) - assert ( - series.y_displacements - == (df["rod_top_y"].iloc[idx:] - df["rod_top_y"].iloc[idx]).to_list() - ) - - # Valid input using start_date_time = 2024-04-11 04:00:00. - series.start_date_time = datetime.datetime(2024, 4, 11, 4, 0, 0) - - df = measurement_series.to_dataframe() - idx = 3 # expected start index - assert len(series.items) == len(df.iloc[idx:]) - assert series.start_date_time == measurement_series.measurements[idx].date_time - assert series.date_times == df["date_time"].to_list()[idx:] - assert series.days == list( - [ - (d - series.start_date_time).total_seconds() / 86400.0 - for d in df["date_time"].to_list()[idx:] - ] - ) - assert ( - series.fill_thicknesses - == (df["ground_surface_z"] - df["rod_bottom_z"]).to_list()[idx:] - ) - assert ( - series.settlements - == (df["rod_bottom_z"].iloc[idx] - df["rod_bottom_z"].iloc[idx:]).to_list() - ) - assert ( - series.x_displacements - == (df["rod_top_x"].iloc[idx:] - df["rod_top_x"].iloc[idx]).to_list() - ) - assert ( - series.y_displacements - == (df["rod_top_y"].iloc[idx:] - df["rod_top_y"].iloc[idx]).to_list() - ) - - -def test_from_settlement_rod_measurement_series_with_invalid_input( +def test_measured_settlement_series_with_invalid_input( example_settlement_rod_measurement_series: SettlementRodMeasurementSeries, ) -> None: - """Test constructor method from_settlement_rod_measurement_series with invalid input.""" + """Test initialization method of MeasuredSettlementSeries with invalid input.""" measurement_series = example_settlement_rod_measurement_series @@ -249,6 +149,132 @@ def test_from_settlement_rod_measurement_series_with_invalid_input( ) +@pytest.mark.parametrize( + "start_index,expected_start_index_or_error", + [ + (0, 0), + (5, 5), + (-2, -2), + (5.0, TypeError), + (20, IndexError), # out of range with positive value + (-20, IndexError), # out of range with negative value + ], +) +def test_measured_settlement_series_start_index_setter( + example_settlement_rod_measurement_series: SettlementRodMeasurementSeries, + start_index: int, + expected_start_index_or_error: int | Type[Exception], +) -> None: + """Test the start_index setter method of MeasuredSettlementSeries.""" + + # Create the series + measurement_series = example_settlement_rod_measurement_series + + series = MeasuredSettlementSeries( + series=measurement_series, + ) + + # Set the start_index and check whether the expected error is + # raised or the expected output is obtained. + if isinstance(expected_start_index_or_error, Exception): + with pytest.raises(expected_start_index_or_error): + series.start_index = start_index + + elif isinstance(expected_start_index_or_error, int): + series.start_index = start_index + + # Check the output + df = measurement_series.to_dataframe() + idx = expected_start_index_or_error + assert len(series.items) == len(df.iloc[idx:]) + assert series.start_date_time == measurement_series.measurements[idx].date_time + assert series.date_times == df["date_time"].to_list()[idx:] + assert series.days == list( + [ + (d - series.start_date_time).total_seconds() / 86400.0 + for d in df["date_time"].to_list()[idx:] + ] + ) + assert ( + series.fill_thicknesses + == (df["ground_surface_z"] - df["rod_bottom_z"]).to_list()[idx:] + ) + assert ( + series.settlements + == (df["rod_bottom_z"].iloc[idx] - df["rod_bottom_z"].iloc[idx:]).to_list() + ) + assert ( + series.x_displacements + == (df["rod_top_x"].iloc[idx:] - df["rod_top_x"].iloc[idx]).to_list() + ) + assert ( + series.y_displacements + == (df["rod_top_y"].iloc[idx:] - df["rod_top_y"].iloc[idx]).to_list() + ) + + +@pytest.mark.parametrize( + "start_date_time,expected_start_index_or_error", + [ + (datetime.datetime(2024, 4, 11, 0, 0, 0), 2), + (datetime.datetime(2024, 4, 11, 4, 0, 0), 3), + ("2024-04-11 00:00:00", TypeError), + (datetime.datetime(2024, 4, 1, 0, 0, 0), ValueError), # date before series + ], +) +def test_measured_settlement_series_start_datetime_setter( + example_settlement_rod_measurement_series: SettlementRodMeasurementSeries, + start_date_time: datetime.datetime, + expected_start_index_or_error: int | Type[Exception], +) -> None: + """Test the start_datetime setter method of MeasuredSettlementSeries.""" + + # Create the series + measurement_series = example_settlement_rod_measurement_series + + series = MeasuredSettlementSeries( + series=measurement_series, + ) + + # Set the start_datetime and check whether the expected error is + # raised or the expected output is obtained. + if isinstance(expected_start_index_or_error, Exception): + with pytest.raises(expected_start_index_or_error): + series.start_date_time = start_date_time + + elif isinstance(expected_start_index_or_error, int): + series.start_date_time = start_date_time + + # Check the output + df = measurement_series.to_dataframe() + idx = expected_start_index_or_error + assert len(series.items) == len(df.iloc[idx:]) + assert series.start_date_time == measurement_series.measurements[idx].date_time + assert series.date_times == df["date_time"].to_list()[idx:] + assert series.days == list( + [ + (d - series.start_date_time).total_seconds() / 86400.0 + for d in df["date_time"].to_list()[idx:] + ] + ) + assert ( + series.fill_thicknesses + == (df["ground_surface_z"] - df["rod_bottom_z"]).to_list()[idx:] + ) + assert ( + series.settlements + == (df["rod_bottom_z"].iloc[idx] - df["rod_bottom_z"].iloc[idx:]).to_list() + ) + assert ( + series.x_displacements + == (df["rod_top_x"].iloc[idx:] - df["rod_top_x"].iloc[idx]).to_list() + ) + assert ( + series.y_displacements + == (df["rod_top_y"].iloc[idx:] - df["rod_top_y"].iloc[idx]).to_list() + ) + + def test_days_to_date_time( example_measured_settlement_series: MeasuredSettlementSeries, ) -> None: @@ -301,6 +327,40 @@ def test_date_time_to_days( series.date_time_to_days(date_time="2024-04-24 00:00:00") +def test_measured_settlement_series_to_dataframe_method( + example_settlement_rod_measurement_series: SettlementRodMeasurementSeries, +) -> None: + """Test the to_dataframe method of MeasuredSettlementSeries.""" + measurement_series = example_settlement_rod_measurement_series + + # Valid input with default start_index and start_date_time. + series = MeasuredSettlementSeries( + series=measurement_series, + ) + + df = series.to_dataframe() + + # Check that the DataFrame has the correct number of rows. + assert len(df) == len(measurement_series.measurements) + + # Check that the DataFrame has the correct data. + for i, item in enumerate(series.items): + assert df.iloc[i]["project_id"] == item.project.id + assert df.iloc[i]["project_name"] == item.project.name + assert df.iloc[i]["object_id"] == item.object_id + assert df.iloc[i]["start_date_time"] == item.start_date_time + assert df.iloc[i]["date_time"] == item.date_time + assert df.iloc[i]["days"] == item.days + assert df.iloc[i]["fill_thickness"] == item.fill_thickness + assert df.iloc[i]["settlement"] == item.settlement + assert df.iloc[i]["x_displacement"] == item.x_displacement + assert df.iloc[i]["y_displacement"] == item.y_displacement + assert df.iloc[i]["horizontal_units"] == item.horizontal_units + assert df.iloc[i]["vertical_units"] == item.vertical_units + assert df.iloc[i]["status"] == item.status.value + assert df.iloc[i]["status_messages"] == "(code=0, description=OK, level=OK)" + + def test_plot_x_displacement_time( example_measured_settlement_series: MeasuredSettlementSeries, ) -> None: diff --git a/tests/measurements/test_settlement_rod_measurement.py b/tests/measurements/test_settlement_rod_measurement.py index 9ce951c..3aae673 100644 --- a/tests/measurements/test_settlement_rod_measurement.py +++ b/tests/measurements/test_settlement_rod_measurement.py @@ -1,4 +1,5 @@ import datetime +from typing import Any, Type import pyproj import pytest @@ -8,10 +9,81 @@ from baec.measurements.settlement_rod_measurement import ( SettlementRodMeasurement, SettlementRodMeasurementStatus, + StatusMessage, + StatusMessageLevel, ) from baec.project import Project +def test_status_message_level_comparison() -> None: + """Test comparison of status message levels.""" + # OK + assert StatusMessageLevel.OK == StatusMessageLevel.OK + assert StatusMessageLevel.OK < StatusMessageLevel.INFO + assert StatusMessageLevel.OK < StatusMessageLevel.WARNING + assert StatusMessageLevel.OK < StatusMessageLevel.ERROR + + # INFO + assert StatusMessageLevel.INFO > StatusMessageLevel.OK + assert StatusMessageLevel.INFO == StatusMessageLevel.INFO + assert StatusMessageLevel.INFO < StatusMessageLevel.WARNING + assert StatusMessageLevel.INFO < StatusMessageLevel.ERROR + + # WARNING + assert StatusMessageLevel.WARNING > StatusMessageLevel.OK + assert StatusMessageLevel.WARNING > StatusMessageLevel.INFO + assert StatusMessageLevel.WARNING == StatusMessageLevel.WARNING + assert StatusMessageLevel.WARNING < StatusMessageLevel.ERROR + + # ERROR + assert StatusMessageLevel.ERROR > StatusMessageLevel.OK + assert StatusMessageLevel.ERROR > StatusMessageLevel.INFO + assert StatusMessageLevel.ERROR > StatusMessageLevel.WARNING + assert StatusMessageLevel.ERROR == StatusMessageLevel.ERROR + + # Different types + assert StatusMessageLevel.OK != 0 + + +def test_status_message_init_with_valid_input() -> None: + """Test initialization of status message with valid input.""" + code = 0 + description = "OK" + level = StatusMessageLevel.OK + + status_message = StatusMessage(code=code, description=description, level=level) + + assert status_message.code == code + assert status_message.description == description + assert status_message.level == level + + +def test_status_message_init_with_invalid_input() -> None: + """Test initialization of status message with invalid input.""" + with pytest.raises(TypeError): + StatusMessage(code="0", description="OK", level=StatusMessageLevel.OK) + + with pytest.raises(TypeError): + StatusMessage(code=0, description=0, level=StatusMessageLevel.OK) + + with pytest.raises(ValueError): + StatusMessage(code=0, description="", level=StatusMessageLevel.OK) + + with pytest.raises(TypeError): + StatusMessage(code=0, description="OK", level=0) + + +def test_status_message_to_string() -> None: + """Test string representation of status message.""" + code = 0 + description = "No comment" + level = StatusMessageLevel.OK + + status_message = StatusMessage(code=code, description=description, level=level) + + assert status_message.to_string() == "(code=0, description=No comment, level=OK)" + + def test_settlement_rod_measurement_init_with_valid_input() -> None: """Test initialization of settlement rod measurement with valid input.""" project = Project(id_="P-001", name="Project 1") @@ -25,10 +97,11 @@ def test_settlement_rod_measurement_init_with_valid_input() -> None: rod_length = 2.0 rod_bottom_z = -1.193 ground_surface_z = 0.419 - status = SettlementRodMeasurementStatus.OK + status_messages = [ + StatusMessage(code=0, description="OK", level=StatusMessageLevel.OK), + ] temperature = 12.0 voltage = 4232 - comment = "No comment" plate_bottom_z_uncorrected = -1.193 measurement = SettlementRodMeasurement( @@ -43,10 +116,9 @@ def test_settlement_rod_measurement_init_with_valid_input() -> None: rod_length=rod_length, rod_bottom_z=rod_bottom_z, ground_surface_z=ground_surface_z, - status=status, + status_messages=status_messages, temperature=temperature, voltage=voltage, - comment=comment, ) assert measurement.project == project @@ -60,402 +132,85 @@ def test_settlement_rod_measurement_init_with_valid_input() -> None: assert measurement.rod_length == rod_length assert measurement.ground_surface_z == ground_surface_z assert measurement.rod_bottom_z == rod_bottom_z - assert measurement.status == status + assert measurement.status_messages == status_messages + assert measurement.status == SettlementRodMeasurementStatus.OK assert measurement.rod_bottom_z_uncorrected == plate_bottom_z_uncorrected assert measurement.temperature == temperature assert measurement.voltage == voltage - assert measurement.comment == comment - - -def test_settlement_rod_measurement_init_with_invalid_project() -> None: - """Test initialization of settlement rod measurement with invalid project.""" - # Invalid project: None - with pytest.raises(TypeError, match="project"): - SettlementRodMeasurement( - project=None, - device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), - object_id="ZB-02", - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( - 28992, 5709 - ), - rod_top_x=123340.266, - rod_top_y=487597.154, - rod_top_z=0.807, - rod_length=2.0, - rod_bottom_z=-1.193, - ground_surface_z=0.419, - status=SettlementRodMeasurementStatus.OK, - temperature=12.0, - voltage=4232, - comment="No comment", - ) - - -def test_settlement_rod_measurement_init_with_invalid_device() -> None: - """Test initialization of settlement rod measurement with invalid device.""" - # Invalid device: None - with pytest.raises(TypeError, match="device"): - SettlementRodMeasurement( - project=Project(id_="P-001", name="Project 1"), - device=None, - object_id="ZB-02", - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( - 28992, 5709 - ), - rod_top_x=123340.266, - rod_top_y=487597.154, - rod_top_z=0.807, - rod_length=2.0, - rod_bottom_z=-1.193, - ground_surface_z=0.419, - status=SettlementRodMeasurementStatus.OK, - temperature=12.0, - voltage=4232, - comment="No comment", - ) - - -def test_settlement_rod_measurement_init_with_invalid_object_id() -> None: - """Test initialization of settlement rod measurement with invalid object_id.""" - # Invalid point_id: None - with pytest.raises(TypeError, match="object_id"): - SettlementRodMeasurement( - project=Project(id_="P-001", name="Project 1"), - device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), - object_id=None, - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( - 28992, 5709 - ), - rod_top_x=123340.266, - rod_top_y=487597.154, - rod_top_z=0.807, - rod_length=2.0, - rod_bottom_z=-1.193, - ground_surface_z=0.419, - status=SettlementRodMeasurementStatus.OK, - temperature=12.0, - voltage=4232, - comment="No comment", - ) - - # Invalid rod_id: Empty string - with pytest.raises(ValueError, match="object_id"): - SettlementRodMeasurement( - project=Project(id_="P-001", name="Project 1"), - device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), - object_id="", - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( - 28992, 5709 - ), - rod_top_x=123340.266, - rod_top_y=487597.154, - rod_top_z=0.807, - rod_length=2.0, - rod_bottom_z=-1.193, - ground_surface_z=0.419, - status=SettlementRodMeasurementStatus.OK, - temperature=12.0, - voltage=4232, - comment="No comment", - ) - - -def test_settlement_rod_measurement_init_with_invalid_date_time() -> None: - """Test initialization of settlement rod measurement with invalid date_time.""" - # Invalid date_time: None - with pytest.raises(TypeError, match="date_time"): - SettlementRodMeasurement( - project=Project(id_="P-001", name="Project 1"), - device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), - object_id="ZB-02", - date_time=None, - coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( - 28992, 5709 - ), - rod_top_x=123340.266, - rod_top_y=487597.154, - rod_top_z=0.807, - rod_length=2.0, - rod_bottom_z=-1.193, - ground_surface_z=0.419, - status=SettlementRodMeasurementStatus.OK, - temperature=12.0, - voltage=4232, - comment="No comment", - ) - - -def test_settlement_rod_measurement_init_with_invalid_coordinate_reference_systems() -> ( - None -): - """Test initialization of settlement rod measurement with invalid coordinate reference systems.""" - # Invalid coordinate_reference_system: None - with pytest.raises(TypeError, match="coordinate_reference_systems"): - SettlementRodMeasurement( - project=Project(id_="P-001", name="Project 1"), - device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), - object_id="ZB-02", - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - coordinate_reference_systems=None, - rod_top_x=123340.266, - rod_top_y=487597.154, - rod_top_z=0.807, - rod_length=2.0, - rod_bottom_z=-1.193, - ground_surface_z=0.419, - status=SettlementRodMeasurementStatus.OK, - temperature=12.0, - voltage=4232, - comment="No comment", - ) - - -def test_settlement_rod_measurement_init_with_invalid_rod_top_x() -> None: - """Test initialization of settlement rod measurement with invalid rod_top_x.""" - # Invalid rod_top_x: String value - with pytest.raises(TypeError, match="rod_top_x"): - SettlementRodMeasurement( - project=Project(id_="P-001", name="Project 1"), - device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), - object_id="ZB-02", - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( - 28992, 5709 - ), - rod_top_x="123340.266", - rod_top_y=487597.154, - rod_top_z=0.807, - rod_length=2.0, - rod_bottom_z=-1.193, - ground_surface_z=0.419, - status=SettlementRodMeasurementStatus.OK, - temperature=12.0, - voltage=4232, - comment="No comment", - ) - - -def test_settlement_rod_measurement_init_with_invalid_rod_top_y() -> None: - """Test initialization of settlement rod measurement with invalid rod_top_y.""" - # Invalid rod_top_y: None - with pytest.raises(TypeError, match="rod_top_y"): - SettlementRodMeasurement( - project=Project(id_="P-001", name="Project 1"), - device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), - object_id="ZB-02", - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( - 28992, 5709 - ), - rod_top_x=123340.266, - rod_top_y=None, - rod_top_z=0.807, - rod_length=2.0, - rod_bottom_z=-1.193, - ground_surface_z=0.419, - status=SettlementRodMeasurementStatus.OK, - temperature=12.0, - voltage=4232, - comment="No comment", - ) - - -def test_settlement_rod_measurement_init_with_rod_top_z() -> None: - """Test initialization of settlement rod measurement with invalid rod_top_z.""" - # Invalid rod_top_z: String value - with pytest.raises(TypeError, match="rod_top_z"): - SettlementRodMeasurement( - project=Project(id_="P-001", name="Project 1"), - device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), - object_id="ZB-02", - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( - 28992, 5709 - ), - rod_top_x=123340.266, - rod_top_y=487597.154, - rod_top_z="0.807", - rod_length=2.0, - rod_bottom_z=-1.193, - ground_surface_z=0.419, - status=SettlementRodMeasurementStatus.OK, - temperature=12.0, - voltage=4232, - comment="No comment", - ) - - -def test_settlement_rod_measurement_init_with_invalid_rod_length() -> None: - """Test initialization of settlement rod measurement with invalid rod_length.""" - # Invalid point_z: String value - with pytest.raises(TypeError, match="rod_length"): - SettlementRodMeasurement( - project=Project(id_="P-001", name="Project 1"), - device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), - object_id="ZB-02", - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( - 28992, 5709 - ), - rod_top_x=123340.266, - rod_top_y=487597.154, - rod_top_z=0.807, - rod_length="2.0", - rod_bottom_z=-1.193, - ground_surface_z=0.419, - status=SettlementRodMeasurementStatus.OK, - temperature=12.0, - voltage=4232, - comment="No comment", - ) - - # Invalid rod_length: Negative value - with pytest.raises(ValueError, match="rod_length"): - SettlementRodMeasurement( - project=Project(id_="P-001", name="Project 1"), - device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), - object_id="ZB-02", - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( - 28992, 5709 - ), - rod_top_x=123340.266, - rod_top_y=487597.154, - rod_top_z=0.807, - rod_length=-2.0, - rod_bottom_z=-1.193, - ground_surface_z=0.419, - status=SettlementRodMeasurementStatus.OK, - temperature=12.0, - voltage=4232, - comment="No comment", - ) - - -def test_settlement_rod_measurement_init_with_invalid_ground_surface_z() -> None: - """Test initialization of settlement rod measurement with invalid ground_surface_z.""" - # Invalid ground_surface_z: String value - with pytest.raises(TypeError, match="ground_surface_z"): - SettlementRodMeasurement( - project=Project(id_="P-001", name="Project 1"), - device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), - object_id="ZB-02", - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( - 28992, 5709 - ), - rod_top_x=123340.266, - rod_top_y=487597.154, - rod_top_z=0.807, - rod_length=2.0, - rod_bottom_z=-1.193, - ground_surface_z="0.419", - status=SettlementRodMeasurementStatus.OK, - temperature=12.0, - voltage=4232, - comment="No comment", - ) - - -def test_settlement_rod_measurement_init_with_invalid_rod_bottom_z() -> None: - """Test initialization of settlement rod measurement with invalid rod_bottom_z.""" - # Invalid rod_bottom_z: String value - with pytest.raises(TypeError, match="rod_bottom_z"): - SettlementRodMeasurement( - project=Project(id_="P-001", name="Project 1"), - device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), - object_id="ZB-02", - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( - 28992, 5709 - ), - rod_top_x=123340.266, - rod_top_y=487597.154, - rod_top_z=0.807, - rod_length=2.0, - rod_bottom_z="-1.193", - ground_surface_z=0.419, - status=SettlementRodMeasurementStatus.OK, - temperature=12.0, - voltage=4232, - comment="No comment", - ) - - -def test_settlement_rod_measurement_init_with_invalid_temperature() -> None: - """Test initialization of settlement rod measurement with invalid temperature.""" - # Invalid temperature: String value - with pytest.raises(TypeError, match="temperature"): - SettlementRodMeasurement( - project=Project(id_="P-001", name="Project 1"), - device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), - object_id="ZB-02", - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( - 28992, 5709 - ), - rod_top_x=123340.266, - rod_top_y=487597.154, - rod_top_z=0.807, - rod_length=2.0, - rod_bottom_z=-1.193, - ground_surface_z=0.419, - status=SettlementRodMeasurementStatus.OK, - temperature="12.0", - voltage=4232, - comment="No comment", - ) - - -def test_settlement_rod_measurement_init_with_invalid_voltage() -> None: - """Test initialization of settlement rod measurement with invalid voltage.""" - # Invalid voltage: String value - with pytest.raises(TypeError, match="voltage"): - SettlementRodMeasurement( - project=Project(id_="P-001", name="Project 1"), - device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), - object_id="ZB-02", - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( - 28992, 5709 - ), - rod_top_x=123340.266, - rod_top_y=487597.154, - rod_top_z=0.807, - rod_length=2.0, - rod_bottom_z=-1.193, - ground_surface_z=0.419, - status=SettlementRodMeasurementStatus.OK, - temperature=12.0, - voltage="4232", - comment="No comment", - ) - - -def test_settlement_rod_measurement_init_with_invalid_comment() -> None: - """Test initialization of settlement rod measurement with invalid comment.""" - # Invalid comment: Integer value - with pytest.raises(TypeError): - SettlementRodMeasurement( - project=Project(id_="P-001", name="Project 1"), - device=MeasurementDevice(id_="BR_003", qr_code="QR-003"), - object_id="ZB-02", - date_time=datetime.datetime(2024, 4, 9, 4, 0, 0), - coordinate_reference_systems=CoordinateReferenceSystems.from_epsg( - 28992, 5709 - ), - rod_top_x=123340.266, - rod_top_y=487597.154, - rod_top_z=0.807, - rod_length=2.0, - rod_bottom_z=-1.193, - ground_surface_z=0.419, - status=SettlementRodMeasurementStatus.OK, - temperature=12.0, - voltage=4232, - comment=123, - ) + + +@pytest.mark.parametrize( + "parameter,invalid_input,expected_error", + [ + ("project", None, TypeError), + ("device", None, TypeError), + ("object_id", None, TypeError), + ("object_id", "", ValueError), + ("date_time", None, TypeError), + ("coordinate_reference_systems", None, TypeError), + ("rod_top_x", "123340.266", TypeError), + ("rod_top_y", None, TypeError), + ("rod_top_z", "0.807", TypeError), + ("rod_length", "2.0", TypeError), + ("rod_length", -2.0, ValueError), + ("ground_surface_z", "0.419", TypeError), + ("rod_bottom_z", "-1.193", TypeError), + ("temperature", "12.0", TypeError), + ("voltage", "4232", TypeError), + ("status_messages", None, TypeError), + ("status_messages", ["OK"], TypeError), + ], +) +def test_settlement_rod_measurement_init_with_invalid_input( + valid_settlement_rod_measurement_input: dict, + parameter: str, + invalid_input: Any, + expected_error: Type[Exception], +) -> None: + """Test initialization of SettlementRodMeasurement with invalid input.""" + valid_settlement_rod_measurement_input[parameter] = invalid_input + with pytest.raises(expected_error, match=parameter): + SettlementRodMeasurement(**valid_settlement_rod_measurement_input) + + +def test_settlement_rod_measurement_status( + valid_settlement_rod_measurement_input: dict, +) -> None: + """Test status property of SettlementRodMeasurement.""" + + # No status messages + valid_settlement_rod_measurement_input["status_messages"] = [] + measurement = SettlementRodMeasurement(**valid_settlement_rod_measurement_input) + assert measurement.status == SettlementRodMeasurementStatus.OK + + # Different status messages with OK as highest level. + valid_settlement_rod_measurement_input["status_messages"] = [ + StatusMessage(code=5, description="OK", level=StatusMessageLevel.OK), + StatusMessage(code=1, description="No comments", level=StatusMessageLevel.OK), + ] + measurement = SettlementRodMeasurement(**valid_settlement_rod_measurement_input) + assert measurement.status == SettlementRodMeasurementStatus.OK + + # Different status messages with INFO as highest level. + valid_settlement_rod_measurement_input["status_messages"] = [ + StatusMessage(code=5, description="OK", level=StatusMessageLevel.OK), + StatusMessage(code=1, description="INFO", level=StatusMessageLevel.INFO), + ] + measurement = SettlementRodMeasurement(**valid_settlement_rod_measurement_input) + assert measurement.status == SettlementRodMeasurementStatus.INFO + + # Different status messages with WARNING as highest level. + valid_settlement_rod_measurement_input["status_messages"] = [ + StatusMessage(code=5, description="WARNING", level=StatusMessageLevel.WARNING), + StatusMessage(code=2, description="INFO", level=StatusMessageLevel.INFO), + ] + measurement = SettlementRodMeasurement(**valid_settlement_rod_measurement_input) + assert measurement.status == SettlementRodMeasurementStatus.WARNING + + # Different status messages with ERROR as highest level. + valid_settlement_rod_measurement_input["status_messages"] = [ + StatusMessage(code=5, description="WARNING", level=StatusMessageLevel.WARNING), + StatusMessage(code=10, description="ERROR", level=StatusMessageLevel.ERROR), + ] + measurement = SettlementRodMeasurement(**valid_settlement_rod_measurement_input) + assert measurement.status == SettlementRodMeasurementStatus.ERROR diff --git a/tests/measurements/test_settlement_rod_measurement_series.py b/tests/measurements/test_settlement_rod_measurement_series.py index 18c6ab9..9d4f986 100644 --- a/tests/measurements/test_settlement_rod_measurement_series.py +++ b/tests/measurements/test_settlement_rod_measurement_series.py @@ -142,9 +142,9 @@ def test_settlement_rod_measurement_series_to_dataframe_method( ) assert df.iloc[i]["ground_surface_z"] == measurement.ground_surface_z assert df.iloc[i]["status"] == measurement.status.value + assert df.iloc[i]["status_messages"] == "(code=0, description=OK, level=OK)" assert df.iloc[i]["temperature"] == measurement.temperature assert df.iloc[i]["voltage"] == measurement.voltage - assert df.iloc[i]["comment"] == measurement.comment def test_plot_x_time(