diff --git a/tests/software_tests/database/data_record/__init__.py b/tests/software_tests/database/data_record/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/software_tests/database/test_abstract_data_record.py b/tests/software_tests/database/data_record/test_abstract_data_record.py similarity index 71% rename from tests/software_tests/database/test_abstract_data_record.py rename to tests/software_tests/database/data_record/test_abstract_data_record.py index 87aff108..230b0251 100644 --- a/tests/software_tests/database/test_abstract_data_record.py +++ b/tests/software_tests/database/data_record/test_abstract_data_record.py @@ -1,9 +1,9 @@ import pytest from mock import Mock, patch -from uds.database.abstract_data_record import AbstractDataRecord +from uds.database.data_record.abstract_data_record import AbstractDataRecord -SCRIPT_LOCATION = "uds.database.abstract_data_record" +SCRIPT_LOCATION = "uds.database.data_record.abstract_data_record" class TestAbstractDataRecord: @@ -35,3 +35,16 @@ def test_init__valid(self, mock_isinstance): def test_name(self): self.mock_data_record._AbstractDataRecord__name = Mock() assert AbstractDataRecord.name.fget(self.mock_data_record) == self.mock_data_record._AbstractDataRecord__name + + # max_raw_value + + @pytest.mark.parametrize( + "length, value", [ + (2, 3), + (5, 31), + (8, 255), + ] + ) + def test_max_raw_value_getter(self, length, value): + self.mock_data_record.length = length + assert AbstractDataRecord.max_raw_value.fget(self.mock_data_record) == value diff --git a/tests/software_tests/database/data_record/test_raw_data_record.py b/tests/software_tests/database/data_record/test_raw_data_record.py new file mode 100644 index 00000000..d5222b3a --- /dev/null +++ b/tests/software_tests/database/data_record/test_raw_data_record.py @@ -0,0 +1,161 @@ +import pytest +from mock import Mock, patch + +from uds.database.data_record.raw_data_record import RawDataRecord + +SCRIPT_LOCATION = "uds.database.data_record.raw_data_record" + + +class TestRawDataRecord: + + def setup_method(self): + self.mock_data_record = Mock(spec=RawDataRecord) + + # __init__ + + @pytest.mark.parametrize( + "name, length", [ + ("TestRawDataRecord", 8), + (Mock(), Mock()), + ] + ) + @patch(f"{SCRIPT_LOCATION}.AbstractDataRecord.__init__") + def test_init__valid(self, mock_abstract, name, length): + assert RawDataRecord.__init__(self.mock_data_record, name, length) is None + mock_abstract.assert_called_once_with(name) + assert self.mock_data_record.length == length + + # length + + def test_length_getter(self): + self.mock_data_record._RawDataRecord__length = Mock() + assert RawDataRecord.length.fget(self.mock_data_record) == self.mock_data_record._RawDataRecord__length + + @pytest.mark.parametrize( + "value", [1, 8] + ) + def test_length_setter_valid(self, value): + RawDataRecord.length.fset(self.mock_data_record, value) + assert self.mock_data_record._RawDataRecord__length == value + + @pytest.mark.parametrize( + "value", ["test", None] + ) + @patch(f"{SCRIPT_LOCATION}.isinstance") + def test_length_setter_type_error(self, mock_isinstance, value): + mock_isinstance.return_value = False + with pytest.raises(TypeError): + RawDataRecord.length.fset(self.mock_data_record, value) + mock_isinstance.assert_called_once_with(value, int) + + @pytest.mark.parametrize( + "value", [0, -1] + ) + def test_length_setter_value_error(self, value): + with pytest.raises(ValueError): + RawDataRecord.length.fset(self.mock_data_record, value) + + # max_raw_value + + @pytest.mark.parametrize( + "length, value", [ + (2, 3), + (5, 31), + (8, 255), + ] + ) + def test_max_raw_value_getter(self, length, value): + raw_data_record = RawDataRecord("TestRawDataRecord", length) + assert raw_data_record.max_raw_value == value + + # is_reoccurring + + def test_is_reoccurring_getter(self): + assert RawDataRecord.is_reoccurring.fget(self.mock_data_record) is False + + # min_occurrences + + def test_min_occurrences_getter(self): + assert RawDataRecord.min_occurrences.fget(self.mock_data_record) == 1 + + # max_occurrences + + def test_max_occurrences_getter(self): + assert RawDataRecord.max_occurrences.fget(self.mock_data_record) == 1 + + # contains + + def test_contains_getter(self): + assert RawDataRecord.contains.fget(self.mock_data_record) == () + + # decode + + @pytest.mark.parametrize( + "value", [1, 4] + ) + @patch(f"{SCRIPT_LOCATION}.DecodedDataRecord") + def test_decode(self, mock_decoded_data_record, value): + self.mock_data_record.max_raw_value = 8 + assert RawDataRecord.decode(self.mock_data_record, value) == mock_decoded_data_record.return_value + mock_decoded_data_record.assert_called_once_with( + name=self.mock_data_record.name, + raw_value=value, + physical_value=value + ) + + @pytest.mark.parametrize( + "value", ["test", None] + ) + @patch(f"{SCRIPT_LOCATION}.isinstance") + def test_decode_type_error(self, mock_isinstance, value): + mock_isinstance.return_value = False + with pytest.raises(TypeError): + RawDataRecord.decode(self.mock_data_record, value) + mock_isinstance.assert_called_once_with(value, int) + + @pytest.mark.parametrize( + "value, max_raw_value", [ + (-1, 2), + (3, 2), + (16, 6), + ] + ) + def test_decode_value_error(self, value, max_raw_value): + self.mock_data_record.max_raw_value = max_raw_value + with pytest.raises(ValueError): + RawDataRecord.decode(self.mock_data_record, value) + + # encode + + @pytest.mark.parametrize( + "value, max_raw_value", [ + (0, 2), + (3, 3), + (16, 16), + ] + ) + def test_encode(self, value, max_raw_value): + self.mock_data_record.max_raw_value = max_raw_value + assert RawDataRecord.encode(self.mock_data_record, value) == value + + @pytest.mark.parametrize( + "value", ["test", None] + ) + @patch(f"{SCRIPT_LOCATION}.isinstance") + def test_encode_type_error(self, mock_isinstance, value): + mock_isinstance.return_value = False + with pytest.raises(TypeError): + RawDataRecord.encode(self.mock_data_record, value) + mock_isinstance.assert_called_once_with(value, int) + + @pytest.mark.parametrize( + "value, max_raw_value", [ + (-1, 2), + (3, 2), + (16, 6), + ] + ) + def test_encode_value_error(self, value, max_raw_value): + self.mock_data_record.max_raw_value = max_raw_value + with pytest.raises(ValueError): + RawDataRecord.encode(self.mock_data_record, value) diff --git a/uds/database/__init__.py b/uds/database/__init__.py index 95a4c9b4..c5b21092 100644 --- a/uds/database/__init__.py +++ b/uds/database/__init__.py @@ -4,5 +4,5 @@ Tools for decoding and encoding information from/to diagnostic messages. """ -from .abstract_data_record import AbstractDataRecord, DataRecordType, DecodedDataRecord +from .data_record import AbstractDataRecord, DecodedDataRecord, RawDataRecord from .services import AbstractService diff --git a/uds/database/data_record/__init__.py b/uds/database/data_record/__init__.py new file mode 100644 index 00000000..3e5c39b5 --- /dev/null +++ b/uds/database/data_record/__init__.py @@ -0,0 +1,9 @@ +""" +Package with implementation for all type of Data Records. + +Each Data Record contains mapping (translation) of raw data (sequence of bits in diagnostic message payload) to some +meaningful information (e.g. physical value, text). +""" + +from .abstract_data_record import AbstractDataRecord, DecodedDataRecord +from .raw_data_record import RawDataRecord diff --git a/uds/database/abstract_data_record.py b/uds/database/data_record/abstract_data_record.py similarity index 77% rename from uds/database/abstract_data_record.py rename to uds/database/data_record/abstract_data_record.py index b26f5eef..fe4afd0a 100644 --- a/uds/database/abstract_data_record.py +++ b/uds/database/data_record/abstract_data_record.py @@ -1,17 +1,10 @@ -""" -Definition of all Data Records types. +"""Definition of AbstractDataRecord which is a base class for all Data Records.""" -Each Data Record contains mapping (translation) of raw data (sequence of bits in diagnostic message payload) to some -meaningful information (e.g. physical value, text). -""" - -__all__ = ["DataRecordType", "AbstractDataRecord", "DecodedDataRecord"] +__all__ = ["AbstractDataRecord", "DataRecordPhysicalValueAlias", "DecodedDataRecord"] from abc import ABC, abstractmethod from typing import Optional, Tuple, TypedDict, Union -from uds.utilities import ValidatedEnum - DataRecordPhysicalValueAlias = Union[int, float, str, Tuple["DecodedDataRecord", ...]] """Alias of Data Records' physical value.""" @@ -24,17 +17,6 @@ class DecodedDataRecord(TypedDict): physical_value: DataRecordPhysicalValueAlias # noqa: F841 -class DataRecordType(ValidatedEnum): - """All Data Record types.""" - - # TODO: fill with following tasks: - # - https://github.com/mdabrowski1990/uds/issues/2 - # - https://github.com/mdabrowski1990/uds/issues/6 - # - https://github.com/mdabrowski1990/uds/issues/8 - # - https://github.com/mdabrowski1990/uds/issues/9 - # - https://github.com/mdabrowski1990/uds/issues/10 - - class AbstractDataRecord(ABC): """Common implementation and interface for all Data Records.""" @@ -55,16 +37,20 @@ def name(self) -> str: """Name of this Data Record.""" return self.__name - @property # noqa: F841 - @abstractmethod - def data_record_type(self) -> DataRecordType: - """Type of this Data Record.""" - @property # noqa: F841 @abstractmethod def length(self) -> int: """Get number of bits that this Data Record is stored over.""" + @property + def max_raw_value(self): + """ + Maximum raw (bit) value for this Data Record. + + :return: Maximum value that can be represented by `length` bits. + """ + return (1 << self.length) - 1 + @property # noqa: F841 @abstractmethod def is_reoccurring(self) -> bool: diff --git a/uds/database/data_record/raw_data_record.py b/uds/database/data_record/raw_data_record.py new file mode 100644 index 00000000..89de1d1c --- /dev/null +++ b/uds/database/data_record/raw_data_record.py @@ -0,0 +1,123 @@ +"""Definition of RawDataRecord.""" + +__all__ = ["RawDataRecord"] + +from typing import Optional, Tuple + +from .abstract_data_record import AbstractDataRecord, DataRecordPhysicalValueAlias, DecodedDataRecord + + +class RawDataRecord(AbstractDataRecord): + """Implementation and interface for Raw Data Record.""" + + def __init__(self, name: str, length: int) -> None: + """ + Initialize Raw Data Record. + + :param name: Name to assign to this Data Record. + :param length: Number of bits that this Raw Data Record is stored over. + + :raise TypeError: Provided name is not str type. + :raise ValueError: Provided length is not a positive integer. + """ + super().__init__(name) + self.length = length + + @property + def length(self) -> int: + """Get number of bits that this Data Record is stored over.""" + return self.__length + + @length.setter + def length(self, value: int) -> None: + """ + Set the length, ensuring it's an integer and within an acceptable range. + + :param value: Number of bits that this Data Record is stored over. + + :raise TypeError: Provided value is not int type. + :raise ValueError: Provided value is less or equal 0. + """ + if not isinstance(value, int): + raise TypeError("Length must be an integer.") + if value <= 0: + raise ValueError("Length must be a positive integer.") + self.__length = value + + @property # noqa: F841 + def is_reoccurring(self) -> bool: + """ + Whether this Data Record might occur multiple times. + + Values meaning: + - False - exactly one occurrence in every diagnostic message + - True - number of occurrences might vary + """ + return False + + @property # noqa: F841 + def min_occurrences(self) -> int: + """ + Minimal number of this Data Record occurrences. + + .. note:: Relevant only if :attr:`~uds.database.data_record.raw_data_record.RawDataRecord.is_reoccurring` + equals True. + """ + return 1 + + @property # noqa: F841 + def max_occurrences(self) -> Optional[int]: + """ + Maximal number of this Data Record occurrences. + + .. note:: Relevant only if :attr:`~uds.database.data_record.raw_data_record.RawDataRecord.is_reoccurring` + equals True. + .. warning:: No maximal number (infinite number of occurrences) is represented by None value. + """ + return 1 + + @property # noqa: F841 + def contains(self) -> Tuple[AbstractDataRecord, ...]: + """Get Data Records contained by this Data Record.""" + return () + + def decode(self, raw_value: int) -> DecodedDataRecord: + """ + Decode physical value for provided raw value. + + :param raw_value: Raw (bit) value of Data Record. + + :return: Dictionary with physical value for this Data Record. + + :raises TypeError: Provided `raw_value` is not int type. + :raises ValueError: Provided `raw_value` is out of range (0 <= raw_value <= max_raw_value). + """ + if not isinstance(raw_value, int): + raise TypeError(f"Expected raw_value to be an int type, got '{type(raw_value).__name__}' instead.") + + if not 0 <= raw_value <= self.max_raw_value: + raise ValueError( + "Provided value of raw_value is out of range: " + f"must be between 0 and {self.max_raw_value}, got {raw_value}." + ) + return DecodedDataRecord(name=self.name, raw_value=raw_value, physical_value=raw_value) + + def encode(self, physical_value: DataRecordPhysicalValueAlias) -> int: + """ + Encode raw value for provided physical value. + + :param physical_value: Physical (meaningful e.g. float, str type) value of this Data Record. + + :return: Raw Value of this Data Record. + """ + if not isinstance(physical_value, int): + raise TypeError( + f"Expected physical_value to be an int type, got '{type(physical_value).__name__}' instead." + ) + + if not 0 <= physical_value <= self.max_raw_value: + raise ValueError( + "Provided value of physical_value is out of range: " + f"must be between 0 and {self.max_raw_value}, got {physical_value}." + ) + return physical_value # type: ignore diff --git a/uds/database/services/abstract_service.py b/uds/database/services/abstract_service.py index 79b74d7f..0bef924c 100644 --- a/uds/database/services/abstract_service.py +++ b/uds/database/services/abstract_service.py @@ -8,7 +8,7 @@ from uds.message import RequestSID, ResponseSID from uds.utilities import RawBytesAlias, RawBytesListAlias -from ..abstract_data_record import DecodedDataRecord +from ..data_record import DecodedDataRecord DataRecordValueAlias = Union[int, float, str, Iterable[Dict[str, "DataRecordValueAlias"]]] "Alias of input with Data Records values."