Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RawDataRecord with unit tests #297

Merged
merged 10 commits into from
Nov 28, 2024
Empty file.
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
161 changes: 161 additions & 0 deletions tests/software_tests/database/data_record/test_raw_data_record.py
mdabrowski1990 marked this conversation as resolved.
Show resolved Hide resolved
n135c10r marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -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):
n135c10r marked this conversation as resolved.
Show resolved Hide resolved
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):
n135c10r marked this conversation as resolved.
Show resolved Hide resolved
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(
n135c10r marked this conversation as resolved.
Show resolved Hide resolved
"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):
n135c10r marked this conversation as resolved.
Show resolved Hide resolved
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)
2 changes: 1 addition & 1 deletion uds/database/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 9 additions & 0 deletions uds/database/data_record/__init__.py
Original file line number Diff line number Diff line change
@@ -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
mdabrowski1990 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -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."""

Expand All @@ -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."""

Expand All @@ -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:
Expand Down
123 changes: 123 additions & 0 deletions uds/database/data_record/raw_data_record.py
mdabrowski1990 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -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)
mdabrowski1990 marked this conversation as resolved.
Show resolved Hide resolved

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
mdabrowski1990 marked this conversation as resolved.
Show resolved Hide resolved
Loading