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

API for Databases #298

Merged
merged 20 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,7 @@ 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

def test_data_record_type(self):
self.mock_data_record.__class__.__name__ = "MockDataRecord"
assert AbstractDataRecord.data_record_type.fget(self.mock_data_record) == "MockDataRecord"
88 changes: 88 additions & 0 deletions tests/software_tests/database/data_record/test_raw_data_record.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import pytest
from mock import 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.name = "TestRawDataRecord"
self.length = 8
self.raw_data_record = RawDataRecord(self.name, self.length)

# __init__

def test_init_valid(self):
assert self.raw_data_record.name == self.name
assert self.raw_data_record.length == self.length

def test_init_type_error_for_name(self):
with pytest.raises(TypeError):
RawDataRecord(123, self.length)

def test_init_value_error_for_length(self):
with pytest.raises(ValueError):
RawDataRecord(self.name, -5)

def test_init_type_error_for_length(self):
with pytest.raises(TypeError):
RawDataRecord(self.name, "eight")

# length

def test_length_getter(self):
assert self.raw_data_record.length == self.length

def test_length_setter_valid(self):
self.raw_data_record.length = 16
assert self.raw_data_record.length == 16

def test_length_setter_type_error(self):
with pytest.raises(TypeError):
self.raw_data_record.length = "sixteen"

def test_length_setter_value_error(self):
with pytest.raises(ValueError):
self.raw_data_record.length = -10

# is_reoccurring

def test_is_reoccurring(self):
assert self.raw_data_record.is_reoccurring is False

# min_occurrences

def test_min_occurrences(self):
assert self.raw_data_record.min_occurrences == 1

# max_occurrences

def test_max_occurrences(self):
assert self.raw_data_record.max_occurrences == 1

# contains

def test_contains(self):
assert self.raw_data_record.contains == ()

# decode

@patch(f"{SCRIPT_LOCATION}.DecodedDataRecord")
def test_decode(self, mock_decoded_data_record):
raw_value = 42
result = self.raw_data_record.decode(raw_value)
mock_decoded_data_record.assert_called_once_with(
name=self.name,
raw_value=raw_value,
physical_value=raw_value
)
assert result == mock_decoded_data_record.return_value

# encode

def test_encode(self):
physical_value = 42
assert self.raw_data_record.encode(physical_value) == physical_value
71 changes: 71 additions & 0 deletions tests/software_tests/database/test_abstract_database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import pytest
from mock import Mock, patch

from uds.database.abstract_database import AbstractDatabase, RequestSID, ResponseSID, UdsMessage, UdsMessageRecord
from uds.transmission_attributes import AddressingType

SCRIPT_LOCATION = "uds.database.abstract_database"

class TestAbstractDatabase:

def setup_method(self):
self.mock_database = Mock(spec=AbstractDatabase)

# encode

@pytest.mark.parametrize("sid", [Mock(), 1])
@pytest.mark.parametrize("data_records_values", [
{"data_record_1": 1, "data_record_2": "Some Value", "data_record_3": 543.234},
{"a": 2.3, "b": 543, "c": "xyz"},
])
@patch(f"{SCRIPT_LOCATION}.isinstance")
def test_encode__type_error(self, mock_isinstance, sid, data_records_values):
mock_isinstance.return_value = False
with pytest.raises(TypeError):
AbstractDatabase.encode(self.mock_database, sid=sid, **data_records_values)
mock_isinstance.assert_called_once_with(sid, int)

@pytest.mark.parametrize("sid", [Mock(), 1, RequestSID.RequestDownload, ResponseSID.WriteDataByIdentifier])
@pytest.mark.parametrize("data_records_values", [
{"data_record_1": 1, "data_record_2": "Some Value", "data_record_3": 543.234},
{"a": 2.3, "b": 543, "c": "xyz"},
])
@patch(f"{SCRIPT_LOCATION}.isinstance")
def test_encode__value_error(self, mock_isinstance, sid, data_records_values):
self.mock_database.services = {}
mock_isinstance.return_value = True
with pytest.raises(ValueError):
AbstractDatabase.encode(self.mock_database, sid=sid, **data_records_values)
mock_isinstance.assert_called_once_with(sid, int)

@pytest.mark.parametrize("sid", [1, RequestSID.RequestDownload, ResponseSID.WriteDataByIdentifier])
@pytest.mark.parametrize("data_records_values", [
{"data_record_1": 1, "data_record_2": "Some Value", "data_record_3": 543.234},
{"a": 2.3, "b": 543, "c": "xyz"},
])
def test_encode(self, sid, data_records_values):
mock_service = Mock()
self.mock_database.services = {sid: mock_service}
assert (AbstractDatabase.encode(self.mock_database, sid=sid, **data_records_values)
== mock_service.encode.return_value)
mock_service.encode.assert_called_once_with(**data_records_values)

# decode

@pytest.mark.parametrize("message", [
UdsMessage(payload=[0x10, 0x03], addressing_type=AddressingType.PHYSICAL),
Mock(spec=UdsMessageRecord, payload=[0x62, *range(255)])
])
def test_decode__value_error(self, message):
self.mock_database.services = {}
with pytest.raises(ValueError):
AbstractDatabase.decode(self.mock_database, message)

@pytest.mark.parametrize("message", [
UdsMessage(payload=[0x10, 0x03], addressing_type=AddressingType.PHYSICAL),
Mock(spec=UdsMessageRecord, payload=[0x62, *range(255)])
])
def test_decode(self, message):
mock_service = Mock()
self.mock_database.services = {message.payload[0]: mock_service}
assert AbstractDatabase.decode(self.mock_database, message) == mock_service.decode.return_value
3 changes: 2 additions & 1 deletion uds/database/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
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
from .abstract_database import AbstractDatabase
75 changes: 75 additions & 0 deletions uds/database/abstract_database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Definition of UDS messages database for data encoding and decoding."""

__all__ = ["AbstractDatabase"]

from abc import ABC, abstractmethod
from typing import Dict, List, Union

from uds.message import RequestSID, ResponseSID, UdsMessage, UdsMessageRecord
from uds.utilities import RawBytesListAlias

from .data_record import DecodedDataRecord
from .services import AbstractService, DataRecordValueAlias


class AbstractDatabase(ABC):
"""Common interface and implementation for UDS message databases."""

@property
@abstractmethod
def services(self) -> Dict[int, AbstractService]:
"""
Get mapping of diagnostic services.

Keys are SID (int) values.
Values are diagnostic services with dedicated decoding and encoding implementation.
"""

def encode(self,
sid: Union[int, RequestSID, ResponseSID],
**data_records_values: DataRecordValueAlias) -> RawBytesListAlias:
"""
Encode diagnostic message payload from data records values.

:param sid: Service Identifier of a diagnostic message.
:param data_records_values: Value for each Data Record that is part a service message.

Each type represent other data:

- int type - raw value of a Data Record
- float type - physical value of a Data Record
- str type - text value of a Data Record
- iterable type - contains values for children Data Records
- dict type - values of children Data Records

.. warning:: Providing physical value as float might sometime cause issues due
`floating-point precision <https://docs.python.org/3/tutorial/floatingpoint.html>`_.
The closest raw value would be evaluated and put into a payload.

To avoid rounding, provide raw value (int type).

:raise TypeError: Provided SID value is neither int, RequestSID nor ResponseSID type.
:raise ValueError: This database has no implementation for provided SID value.

:return: Payload of a diagnostic message.
"""
if not isinstance(sid, int):
raise TypeError("Provided SID value is not int type.")
if sid not in self.services:
raise ValueError("Database has no encoding defined for provided SID value.")
return self.services[sid].encode(**data_records_values)

def decode(self, message: Union[UdsMessage, UdsMessageRecord]) -> List[DecodedDataRecord]:
mdabrowski1990 marked this conversation as resolved.
Show resolved Hide resolved
"""
Decode physical values carried in payload of a diagnostic message.

:param message: A diagnostic message that is carrying payload to decode.

:raise ValueError: This database has no service implementation for provided diagnostic message SID.

:return: Decoded Data Records values from provided diagnostic message.
"""
sid = message.payload[0]
if sid not in self.services:
raise ValueError("Database has no decoding defined for SID value of provided message.")
return self.services[sid].decode(message.payload)
11 changes: 11 additions & 0 deletions uds/database/data_record/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""
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).
"""

__all__ = ["AbstractDataRecord", "DecodedDataRecord", "RawDataRecord"]

from .abstract_data_record import AbstractDataRecord, DecodedDataRecord
from .raw_data_record import RawDataRecord
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 @@ -56,9 +38,9 @@ def name(self) -> str:
return self.__name

@property # noqa: F841
@abstractmethod
def data_record_type(self) -> DataRecordType:
def data_record_type(self) -> str:
"""Type of this Data Record."""
return self.__class__.__name__

@property # noqa: F841
@abstractmethod
Expand Down
Loading
Loading