From 7199236eb44c65f7c5ae9db6cd3d8f6fdd411399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20D=C4=85browski?= <51504507+mdabrowski1990@users.noreply.github.com> Date: Sat, 25 Sep 2021 12:26:49 +0200 Subject: [PATCH] Segmentation API defined + pyroma fix + safety automated in CI (#111) - pyroma execution fix - payload attribute remuval for packet classes - add dependency checking with safety package - define UDS Segmentation API - prepare user documentation for Segmentation - update Segmentation chapter in knowledge base --- .github/workflows/ci.yml | 125 ++++++---- docs/source/index.rst | 1 + .../pages/knowledge_base/segmentation.rst | 16 +- docs/source/pages/message.rst | 6 +- docs/source/pages/segmentation.rst | 29 +++ setup.cfg | 9 + tests/prospector_profile.yaml | 6 +- .../requirements_for_dependency_scanning.txt | 1 + .../messages/test_uds_message.py | 18 +- tests/software_tests/segmentation/__init__.py | 0 .../segmentation/test_abstract_segmenter.py | 217 ++++++++++++++++++ uds/__init__.py | 2 +- uds/messages/__init__.py | 6 +- uds/messages/uds_message.py | 4 +- uds/messages/uds_packet.py | 44 ++-- uds/segmentation/__init__.py | 8 + uds/segmentation/abstract_segmenter.py | 155 +++++++++++++ 17 files changed, 552 insertions(+), 95 deletions(-) create mode 100644 docs/source/pages/segmentation.rst create mode 100644 tests/requirements_for_dependency_scanning.txt create mode 100644 tests/software_tests/segmentation/__init__.py create mode 100644 tests/software_tests/segmentation/test_abstract_segmenter.py create mode 100644 uds/segmentation/__init__.py create mode 100644 uds/segmentation/abstract_segmenter.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c3f1738..61fedfbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,30 +13,30 @@ jobs: python-version: [3.7, 3.8, 3.9] steps: - - uses: actions/checkout@v2 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - - name: Install project dependencies - run: | - python -m pip install --upgrade pip - python -m pip install --upgrade setuptools - pip install -r requirements.txt - - - name: Install external packages used for dynamic tests - run: | - pip install -r tests/requirements_for_software_tests.txt - - - name: Execute unit tests [pytest] - run: | - pytest --cov-report term-missing --cov=uds tests/software_tests -m "not integration" + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install project dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade setuptools + pip install -r requirements.txt - - name: Execute integration tests [pytest] - run: | - pytest tests/software_tests -m integration + - name: Install external packages used for dynamic tests + run: | + pip install -r tests/requirements_for_software_tests.txt + + - name: Execute unit tests [pytest] + run: | + pytest --cov-report term-missing --cov=uds tests/software_tests -m "not integration" + + - name: Execute integration tests [pytest] + run: | + pytest tests/software_tests -m integration static_code_analysis: @@ -46,27 +46,60 @@ jobs: python-version: [3.7, 3.8, 3.9] steps: - - uses: actions/checkout@v2 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - - name: Install project dependencies - run: | - python -m pip install --upgrade pip - python -m pip install --upgrade setuptools - pip install -r requirements.txt - - - name: Install external packages used for static tests - run: | - pip install -r tests/requirements_for_static_code_analysis.txt + - uses: actions/checkout@v2 - - name: Uninstall pylint-django (due to issue https://github.com/PyCQA/prospector/issues/443) - run: | - pip uninstall pylint-django -y - - - name: Execute static code analysis [prospector] - run: | - prospector --profile tests/prospector_profile.yaml uds + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install project dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade setuptools + pip install -r requirements.txt + + - name: Install external packages used for static tests + run: | + pip install -r tests/requirements_for_static_code_analysis.txt + + - name: Uninstall pylint-django (due to issue https://github.com/PyCQA/prospector/issues/443) + run: | + pip uninstall pylint-django -y + + - name: Execute static code analysis [prospector] + run: | + prospector --profile tests/prospector_profile.yaml uds + + - name: Execute static code analysis [pyroma] + run: | + pyroma -a . + + + dependency_checks: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install project dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade setuptools + pip install -r requirements.txt + + - name: Install external packages used for dependency scanning + run: | + pip install -r tests/requirements_for_dependency_scanning.txt + + - name: Execute dependency scanning [safety] + run: | + safety check --full-report diff --git a/docs/source/index.rst b/docs/source/index.rst index 20aba969..50241d2e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,6 +6,7 @@ Welcome to UDS documentation! Home pages/installation.rst pages/message.rst + pages/segmentation.rst pages/transport.rst pages/client_simulation.rst pages/server_simulation.rst diff --git a/docs/source/pages/knowledge_base/segmentation.rst b/docs/source/pages/knowledge_base/segmentation.rst index 2b428e2e..9ae9e146 100644 --- a/docs/source/pages/knowledge_base/segmentation.rst +++ b/docs/source/pages/knowledge_base/segmentation.rst @@ -1,9 +1,21 @@ .. _knowledge-base-segmentation: -.. _knowledge-base-desegmentation: - Segmentation ============ + + +.. _knowledge-base-message-segmentation: + +Message Segmentation +-------------------- If diagnostic message data to be transmitted does not fit into a single frame, then segmentation process is required to divide :ref:`diagnostic message ` into smaller pieces called :ref:`UDS packets `. + + +.. _knowledge-base-packets-desegmentation: + +Packets Desegmentation +---------------------- +Desegmentation is a reverse process to a `message segmentation`_. It transforms one or more +:ref:`UDS packets ` into a :ref:`diagnostic message `. diff --git a/docs/source/pages/message.rst b/docs/source/pages/message.rst index 6e6d7987..01483812 100644 --- a/docs/source/pages/message.rst +++ b/docs/source/pages/message.rst @@ -119,7 +119,7 @@ UDS Packet Each UDS packet class provides containers for :ref:`Network Protocol Data Unit (N_PDU) ` information that are specific for a communication bus for which this class is relevant. **Objects of UDS packet classes might be used to execute complex operations** (provided in other subpackages) such as -packets transmission or :ref:`desegmentation `. +packets transmission or :ref:`desegmentation `. Implemented UDS packet classes: - `AbstractUdsPacket`_ @@ -138,8 +138,6 @@ Properties implemented in :class:`~uds.messages.uds_packet.AbstractUdsPacket` cl - :attr:`~uds.messages.uds_packet.AbstractUdsPacket.raw_data` - settable - :attr:`~uds.messages.uds_packet.AbstractUdsPacket.addressing` - settable - :attr:`~uds.messages.uds_packet.AbstractUdsPacket.packet_type` - readable - - :attr:`~uds.messages.uds_packet.AbstractUdsPacket.packet_type_enum` - readable and abstract (bus specific) - - :attr:`~uds.messages.uds_packet.AbstractUdsPacket.payload` - readable and abstract (bus specific) UDS Packet Record @@ -169,10 +167,8 @@ Properties implemented in :class:`~uds.messages.uds_packet.AbstractUdsPacketReco - :attr:`~uds.messages.uds_packet.AbstractUdsPacketRecord.direction` - readable - :attr:`~uds.messages.uds_packet.AbstractUdsPacketRecord.packet_type` - readable - :attr:`~uds.messages.uds_packet.AbstractUdsPacketRecord.raw_data` - readable and abstract (bus specific) - - :attr:`~uds.messages.uds_packet.AbstractUdsPacketRecord.payload` - readable and abstract (bus specific) - :attr:`~uds.messages.uds_packet.AbstractUdsPacketRecord.addressing` - readable and abstract (bus specific) - :attr:`~uds.messages.uds_packet.AbstractUdsPacketRecord.transmission_time` - readable and abstract (bus specific) - - :attr:`~uds.messages.uds_packet.AbstractUdsPacketRecord.packet_type_enum` - readable and abstract (bus specific) UDS Messages Data diff --git a/docs/source/pages/segmentation.rst b/docs/source/pages/segmentation.rst new file mode 100644 index 00000000..31b8b92c --- /dev/null +++ b/docs/source/pages/segmentation.rst @@ -0,0 +1,29 @@ +Segmentation +============ +Implementation related to :ref:`segmentation ` is located in :mod:`uds.segmentation` +sub-package. + + +AbstractSegmenter +----------------- +:class:`~uds.segmentation.abstract_segmenter.AbstractSegmenter` defines common API and contains common code for all +segmenter classes. Each concrete segmenter class implements segmentation +`strategy `_ for a specific bus. + +A **user shall not use** :class:`~uds.segmentation.abstract_segmenter.AbstractSegmenter` **directly**, but one is able +(and encouraged) to use :class:`~uds.segmentation.abstract_segmenter.AbstractSegmenter` implementation with any of its +children classes. + +Attributes defined in :class:`~uds.messages.uds_packet.AbstractUdsPacketType` class: + - :attr:`~uds.segmentation.abstract_segmenter.AbstractSegmenter.supported_packet_classes` - readable and abstract (bus specific) + - :attr:`~uds.segmentation.abstract_segmenter.AbstractSegmenter.initial_packet_types` - readable and abstract (bus specific) + +Methods defined in :class:`~uds.messages.uds_packet.AbstractUdsPacketType` class: + - :meth:`~uds.segmentation.abstract_segmenter.AbstractSegmenter.is_supported_packet` + - :meth:`~uds.segmentation.abstract_segmenter.AbstractSegmenter.is_supported_packets_sequence` + - :meth:`~uds.segmentation.abstract_segmenter.AbstractSegmenter.is_initial_packet` + - :meth:`~uds.segmentation.abstract_segmenter.AbstractSegmenter.get_consecutive_packets_number` + - :meth:`~uds.segmentation.abstract_segmenter.AbstractSegmenter.is_following_packets_sequence` + - :meth:`~uds.segmentation.abstract_segmenter.AbstractSegmenter.is_complete_packets_sequence` + - :meth:`~uds.segmentation.abstract_segmenter.AbstractSegmenter.segmentation` + - :meth:`~uds.segmentation.abstract_segmenter.AbstractSegmenter.desegmentation` diff --git a/setup.cfg b/setup.cfg index 9dabcb72..2b3d724d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,6 +35,15 @@ classifiers = package_dir = =uds python_requires = >=3.6 +keywords = + automotive + vehicle-diagnostic + uds + unified-diagnostic-services + iso14229 + iso-14229, + obd + on-board-diagnostic [options] diff --git a/tests/prospector_profile.yaml b/tests/prospector_profile.yaml index 5863b9b7..c2e763e1 100644 --- a/tests/prospector_profile.yaml +++ b/tests/prospector_profile.yaml @@ -55,8 +55,8 @@ mypy: vulture: run: true -pyroma: +bandit: # there is probably no use of it (not applicable for this project), but it is run anyways to make sure everything is secured run: true -bandit: # there is probably no use of it (not applicable for this project), but it is run anyways to make sure everything is secured - run: true \ No newline at end of file +pyroma: + run: false diff --git a/tests/requirements_for_dependency_scanning.txt b/tests/requirements_for_dependency_scanning.txt new file mode 100644 index 00000000..0a636ad4 --- /dev/null +++ b/tests/requirements_for_dependency_scanning.txt @@ -0,0 +1 @@ +safety>=1.10.3 \ No newline at end of file diff --git a/tests/software_tests/messages/test_uds_message.py b/tests/software_tests/messages/test_uds_message.py index 83c8c67e..d2f45ea9 100644 --- a/tests/software_tests/messages/test_uds_message.py +++ b/tests/software_tests/messages/test_uds_message.py @@ -88,19 +88,13 @@ def teardown(self): # __init__ - @pytest.mark.parametrize("packets_records, payload", [ - ([Mock(payload=[1])], (1,)), - ([Mock(payload=[0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00])], - (0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00)), - ([Mock(payload=[0x12, 0x34, 0x56]), Mock(payload=(0x78, 0x9A, 0xBC, 0xDE, 0xF0))], - (0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0)), - ((Mock(payload=(0,)), Mock(payload=(1, 2)), Mock(payload=(3, 4, 5, 6, 7, 8)), Mock(payload=[9])), - tuple(range(10))) - ]) - def test_init(self, packets_records, payload): - UdsMessageRecord.__init__(self=self.mock_uds_message_record, packets_records=packets_records) - assert self.mock_uds_message_record.packets_records == packets_records + @pytest.mark.parametrize("payload", [None, [0x1, 0x02], "some message"]) + @pytest.mark.parametrize("packets_records", [False, [1, 2, 3, 4], "abcdef"]) + def test_init(self, payload, packets_records): + UdsMessageRecord.__init__(self=self.mock_uds_message_record, payload=payload, + packets_records=packets_records) assert self.mock_uds_message_record.payload == payload + assert self.mock_uds_message_record.packets_records == packets_records # __validate_packets_records diff --git a/tests/software_tests/segmentation/__init__.py b/tests/software_tests/segmentation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/software_tests/segmentation/test_abstract_segmenter.py b/tests/software_tests/segmentation/test_abstract_segmenter.py new file mode 100644 index 00000000..b330e9cf --- /dev/null +++ b/tests/software_tests/segmentation/test_abstract_segmenter.py @@ -0,0 +1,217 @@ +import pytest +from mock import Mock, patch, MagicMock + +from uds.segmentation.abstract_segmenter import AbstractSegmenter, SegmentationError, \ + UdsMessage, UdsMessageRecord +from uds.messages import AbstractUdsPacket, AbstractUdsPacketRecord + + +class TestAbstractSegmenter: + """Tests for `AbstractSegmenter` class.""" + + SCRIPT_PATH = "uds.segmentation.abstract_segmenter" + + def setup(self): + self.mock_abstract_segmenter = Mock(spec=AbstractSegmenter) + + # is_supported_packet + + @pytest.mark.parametrize("result", [True, False]) + @pytest.mark.parametrize("value", [None, 5, "some value", Mock()]) + @patch(f"{SCRIPT_PATH}.isinstance") + def test_is_supported_packet(self, mock_isinstance, value, result): + mock_isinstance.return_value = result + assert AbstractSegmenter.is_supported_packet(self=self.mock_abstract_segmenter, value=value) is result + mock_isinstance.assert_called_once_with(value, self.mock_abstract_segmenter.supported_packet_classes) + + # is_supported_packets_sequence + + @pytest.mark.parametrize("value", [None, True, 1, Mock(), {1, 2, 3}]) + def test_is_supported_packets_sequence__false__invalid_type(self, value): + assert AbstractSegmenter.is_supported_packets_sequence(self=self.mock_abstract_segmenter, value=value) is False + self.mock_abstract_segmenter.is_supported_packet.assert_not_called() + + @pytest.mark.parametrize("value", [ + (1, 2, 3, 4), + (1, 2.1, 3, 4.4), + [2.2, 3.3, 4.4], + [None, True, False], + (True, False), + ]) + def test_is_supported_packets_sequence__false__invalid_elements_type(self, value): + self.mock_abstract_segmenter.is_supported_packet.return_value = False + assert AbstractSegmenter.is_supported_packets_sequence(self=self.mock_abstract_segmenter, value=value) is False + self.mock_abstract_segmenter.is_supported_packet.assert_called_once() + + @pytest.mark.parametrize("value", [ + (1, 2.1, 3, 4.4), + [None, True, False], + ]) + def test_is_supported_packets_sequence__false__more_element_types(self, value): + self.mock_abstract_segmenter.is_supported_packet.return_value = True + assert AbstractSegmenter.is_supported_packets_sequence(self=self.mock_abstract_segmenter, value=value) is False + self.mock_abstract_segmenter.is_supported_packet.assert_called() + + def test_is_supported_packets_sequence__false__empty_sequence(self): + self.mock_abstract_segmenter.is_supported_packet.return_value = True + assert AbstractSegmenter.is_supported_packets_sequence(self=self.mock_abstract_segmenter, value=[]) is False + + @pytest.mark.parametrize("value", [ + (1, 2, 3, 4), + [2.2, 3.3, 4.4], + (True, False), + ]) + def test_is_supported_packets_sequence__true(self, value): + self.mock_abstract_segmenter.is_supported_packet.return_value = True + assert AbstractSegmenter.is_supported_packets_sequence(self=self.mock_abstract_segmenter, value=value) is True + self.mock_abstract_segmenter.is_supported_packet.assert_called() + + # is_initial_packet + + @pytest.mark.parametrize("packet", [Mock(spec=AbstractUdsPacket), Mock(spec=AbstractUdsPacket)]) + def test_is_initial_packet__type_error(self, packet): + self.mock_abstract_segmenter.is_supported_packet.return_value = False + with pytest.raises(TypeError): + AbstractSegmenter.is_initial_packet(self=self.mock_abstract_segmenter, packet=packet) + self.mock_abstract_segmenter.is_supported_packet.assert_called_once_with(packet) + + @pytest.mark.parametrize("packet", [Mock(spec=AbstractUdsPacket), Mock(spec=AbstractUdsPacket)]) + def test_is_initial_packet__false(self, packet): + self.mock_abstract_segmenter.is_supported_packet.return_value = True + mock_initial_packet_types = MagicMock() + mock_initial_packet_types.__contains__.return_value = False + self.mock_abstract_segmenter.initial_packet_types = mock_initial_packet_types + assert AbstractSegmenter.is_initial_packet(self=self.mock_abstract_segmenter, packet=packet) is False + self.mock_abstract_segmenter.is_supported_packet.assert_called_once_with(packet) + mock_initial_packet_types.__contains__.assert_called_once_with(packet.packet_type) + + @pytest.mark.parametrize("packet", [Mock(spec=AbstractUdsPacket), Mock(spec=AbstractUdsPacket)]) + def test_is_initial_packet__true(self, packet): + self.mock_abstract_segmenter.is_supported_packet.return_value = True + mock_initial_packet_types = MagicMock() + mock_initial_packet_types.__contains__.return_value = True + self.mock_abstract_segmenter.initial_packet_types = mock_initial_packet_types + assert AbstractSegmenter.is_initial_packet(self=self.mock_abstract_segmenter, packet=packet) is True + self.mock_abstract_segmenter.is_supported_packet.assert_called_once_with(packet) + mock_initial_packet_types.__contains__.assert_called_once_with(packet.packet_type) + + # get_consecutive_packets_number + + @pytest.mark.parametrize("packet", [Mock(spec=AbstractUdsPacket), Mock(spec=AbstractUdsPacketRecord)]) + def test_get_consecutive_packets_number__value_error(self, packet): + self.mock_abstract_segmenter.is_initial_packet.return_value = False + with pytest.raises(ValueError): + AbstractSegmenter.get_consecutive_packets_number(self=self.mock_abstract_segmenter, first_packet=packet) + self.mock_abstract_segmenter.is_initial_packet.assert_called_once_with(packet) + + @pytest.mark.parametrize("packet", [Mock(spec=AbstractUdsPacket), Mock(spec=AbstractUdsPacketRecord)]) + def test_get_consecutive_packets_number__valid(self, packet): + self.mock_abstract_segmenter.is_initial_packet.return_value = True + AbstractSegmenter.get_consecutive_packets_number(self=self.mock_abstract_segmenter, first_packet=packet) + self.mock_abstract_segmenter.is_initial_packet.assert_called_once_with(packet) + + # is_following_packets_sequence + + @pytest.mark.parametrize("packets", [ + (1, 2, 3, 4), + (1, 2.1, 3, 4.4), + [2.2, 3.3, 4.4], + [None, True, False], + (True, False), + ]) + def test_is_following_packets_sequence__value_error(self, packets): + self.mock_abstract_segmenter.is_supported_packets_sequence.return_value = False + with pytest.raises(ValueError): + AbstractSegmenter.is_following_packets_sequence(self=self.mock_abstract_segmenter, packets=packets) + self.mock_abstract_segmenter.is_supported_packets_sequence.assert_called_once_with(packets) + self.mock_abstract_segmenter.is_initial_packet.assert_not_called() + + @pytest.mark.parametrize("packets", [ + (1, 2, 3, 4), + (1, 2.1, 3, 4.4), + [2.2, 3.3, 4.4], + [None, True, False], + (True, False), + ]) + def test_is_following_packets_sequence__false(self, packets): + self.mock_abstract_segmenter.is_supported_packets_sequence.return_value = True + self.mock_abstract_segmenter.is_initial_packet.return_value = False + assert AbstractSegmenter.is_following_packets_sequence(self=self.mock_abstract_segmenter, packets=packets) is False + self.mock_abstract_segmenter.is_supported_packets_sequence.assert_called_once_with(packets) + self.mock_abstract_segmenter.is_initial_packet.assert_called_once_with(packets[0]) + + @pytest.mark.parametrize("packets", [ + (1, 2, 3, 4), + (1, 2.1, 3, 4.4), + [2.2, 3.3, 4.4], + [None, True, False], + (True, False), + ]) + def test_is_following_packets_sequence__valid(self, packets): + self.mock_abstract_segmenter.is_supported_packets_sequence.return_value = True + self.mock_abstract_segmenter.is_initial_packet.return_value = True + assert AbstractSegmenter.is_following_packets_sequence(self=self.mock_abstract_segmenter, packets=packets) is None + self.mock_abstract_segmenter.is_supported_packets_sequence.assert_called_once_with(packets) + self.mock_abstract_segmenter.is_initial_packet.assert_called_once_with(packets[0]) + + # is_complete_packets_sequence + + @pytest.mark.parametrize("packets", [ + (Mock(spec=AbstractUdsPacket), Mock(spec=AbstractUdsPacket)), + [Mock(spec=AbstractUdsPacketRecord), Mock(spec=AbstractUdsPacketRecord), Mock(spec=AbstractUdsPacketRecord)], + ]) + def test_is_complete_packets_sequence__not_a_sequence(self, packets): + self.mock_abstract_segmenter.is_following_packets_sequence.return_value = False + assert AbstractSegmenter.is_complete_packets_sequence(self=self.mock_abstract_segmenter, packets=packets) is False + self.mock_abstract_segmenter.is_following_packets_sequence.assert_called_once_with(packets) + self.mock_abstract_segmenter.get_consecutive_packets_number.assert_not_called() + + @pytest.mark.parametrize("packets", [ + (Mock(spec=AbstractUdsPacket), Mock(spec=AbstractUdsPacket)), + [Mock(spec=AbstractUdsPacketRecord), Mock(spec=AbstractUdsPacketRecord), Mock(spec=AbstractUdsPacketRecord)], + ]) + def test_is_complete_packets_sequence__invalid_packets_number(self, packets): + mock_eq_false = MagicMock() + mock_eq_false.__eq__.return_value = False + self.mock_abstract_segmenter.is_following_packets_sequence.return_value = True + self.mock_abstract_segmenter.get_consecutive_packets_number.return_value = mock_eq_false + assert AbstractSegmenter.is_complete_packets_sequence(self=self.mock_abstract_segmenter, packets=packets) is False + self.mock_abstract_segmenter.is_following_packets_sequence.assert_called_once_with(packets) + self.mock_abstract_segmenter.get_consecutive_packets_number.assert_called_once_with(packets[0]) + mock_eq_false.__eq__.assert_called_once_with(len(packets)) + + @pytest.mark.parametrize("packets", [ + (Mock(spec=AbstractUdsPacket), Mock(spec=AbstractUdsPacket)), + [Mock(spec=AbstractUdsPacketRecord), Mock(spec=AbstractUdsPacketRecord), Mock(spec=AbstractUdsPacketRecord)], + ]) + def test_is_complete_packets_sequence__true(self, packets): + self.mock_abstract_segmenter.is_following_packets_sequence.return_value = True + self.mock_abstract_segmenter.get_consecutive_packets_number.return_value = len(packets) + assert AbstractSegmenter.is_complete_packets_sequence(self=self.mock_abstract_segmenter, packets=packets) is True + self.mock_abstract_segmenter.is_following_packets_sequence.assert_called_once_with(packets) + self.mock_abstract_segmenter.get_consecutive_packets_number.assert_called_once_with(packets[0]) + + # segmentation + + @pytest.mark.parametrize("message", [None, False, Mock(spec=UdsMessageRecord), (0x1, 0x2, 0x3)]) + def test_segmentation__type_error(self, message): + with pytest.raises(TypeError): + AbstractSegmenter.segmentation(self=self.mock_abstract_segmenter, message=message) + + def test_segmentation__valid_input(self): + AbstractSegmenter.segmentation(self=self.mock_abstract_segmenter, message=Mock(spec=UdsMessage)) + + # desegmentation + + @pytest.mark.parametrize("packets", [None, "some packets", [1, 2, 3]]) + def test_desegmentation__segmentation_error(self, packets): + self.mock_abstract_segmenter.is_complete_packets_sequence.return_value = False + with pytest.raises(SegmentationError): + AbstractSegmenter.desegmentation(self=self.mock_abstract_segmenter, packets=packets) + self.mock_abstract_segmenter.is_complete_packets_sequence.assert_called_once_with(packets) + + @pytest.mark.parametrize("packets", [None, "some packets", [1, 2, 3]]) + def test_desegmentation__valid_input(self, packets): + self.mock_abstract_segmenter.is_complete_packets_sequence.return_value = True + AbstractSegmenter.desegmentation(self=self.mock_abstract_segmenter, packets=packets) + self.mock_abstract_segmenter.is_complete_packets_sequence.assert_called_once_with(packets) diff --git a/uds/__init__.py b/uds/__init__.py index f6fa120c..289cda7f 100644 --- a/uds/__init__.py +++ b/uds/__init__.py @@ -18,6 +18,6 @@ """ __all__ = ["messages"] -__version__ = "0.0.2" +__version__ = "0.1.0" __author__ = "Maciej DÄ…browski" __email__ = "uds-package-development@googlegroups.com" diff --git a/uds/messages/__init__.py b/uds/messages/__init__.py index ad79a64a..7792095a 100644 --- a/uds/messages/__init__.py +++ b/uds/messages/__init__.py @@ -11,7 +11,11 @@ - addressing types definition """ -from .uds_packet import AbstractUdsPacketType, AbstractUdsPacket, AbstractUdsPacketRecord +from .uds_packet import AbstractUdsPacketType, AbstractUdsPacket, AbstractUdsPacketRecord, \ + PacketTyping, PacketsTuple, PacketsSequence, \ + PacketsDefinitionTuple, PacketsDefinitionSequence, \ + PacketsRecordsTuple, PacketsRecordsSequence, \ + PacketTypesTuple from .uds_message import UdsMessage, UdsMessageRecord from .service_identifiers import RequestSID, ResponseSID, POSSIBLE_REQUEST_SIDS, POSSIBLE_RESPONSE_SIDS, \ UnrecognizedSIDWarning diff --git a/uds/messages/uds_message.py b/uds/messages/uds_message.py index a65c13cc..d7c28f90 100644 --- a/uds/messages/uds_message.py +++ b/uds/messages/uds_message.py @@ -66,15 +66,15 @@ def addressing(self, value: AddressingMemberTyping): class UdsMessageRecord: """Storage for historic information of a diagnostic message that was either received or transmitted.""" - def __init__(self, packets_records: PacketsRecordsSequence) -> None: + def __init__(self, payload: RawBytes, packets_records: PacketsRecordsSequence) -> None: """ Create a record of a historic information about a diagnostic message that was either received or transmitted. :param packets_records: Sequence (in transmission order) of UDS packets records that carried this diagnostic message. """ + self.payload = payload # type: ignore self.packets_records = packets_records # type: ignore - self.payload = tuple(payload_byte for packet in packets_records for payload_byte in packet.payload) @staticmethod def __validate_packets_records(value: Any) -> None: diff --git a/uds/messages/uds_packet.py b/uds/messages/uds_packet.py index 6219ea64..89a65e86 100644 --- a/uds/messages/uds_packet.py +++ b/uds/messages/uds_packet.py @@ -5,7 +5,10 @@ """ __all__ = ["AbstractUdsPacketType", "AbstractUdsPacket", "AbstractUdsPacketRecord", - "PacketsRecordsTuple", "PacketsRecordsSequence"] + "PacketTyping", "PacketsTuple", "PacketsSequence", + "PacketsDefinitionTuple", "PacketsDefinitionSequence", + "PacketsRecordsTuple", "PacketsRecordsSequence", + "PacketTypesTuple"] from abc import ABC, abstractmethod from typing import Union, Tuple, List, Any @@ -71,16 +74,6 @@ def raw_data(self, value: RawBytes): validate_raw_bytes(value) self.__raw_data = tuple(value) - @property - @abstractmethod - def payload(self) -> RawBytesTuple: - """Diagnostic message payload carried by this packet.""" - - @property # noqa: F841 - @abstractmethod - def packet_type_enum(self) -> type: - """Enum with possible UDS packet types.""" - @property # noqa: F841 @abstractmethod def packet_type(self) -> AbstractUdsPacketType: @@ -161,11 +154,6 @@ def direction(self, value: DirectionMemberTyping): def raw_data(self) -> RawBytesTuple: """Raw bytes of data that this packet carried.""" - @property - @abstractmethod - def payload(self) -> RawBytesTuple: - """Diagnostic message payload carried by this packet.""" - @property @abstractmethod def addressing(self) -> AddressingType: @@ -176,18 +164,28 @@ def addressing(self) -> AddressingType: def transmission_time(self) -> TimeStamp: """Time stamp when this packet was fully transmitted on a bus.""" - @property # noqa: F841 - @abstractmethod - def packet_type_enum(self) -> type: - """Enum with possible UDS packet types.""" - @property # noqa: F841 @abstractmethod def packet_type(self) -> AbstractUdsPacketType: """UDS packet type value - N_PCI value of this N_PDU.""" +PacketTypesTuple = Tuple[AbstractUdsPacketType, ...] +"""Typing alias of a tuple filled with :class:`~uds.messages.uds_packet.AbstractUdsPacketType` members.""" + +PacketsDefinitionTuple = Tuple[AbstractUdsPacket, ...] +"""Typing alias of a tuple filled with :class:`~uds.messages.uds_packet.AbstractUdsPacket` instances.""" +PacketsDefinitionSequence = Union[PacketsDefinitionTuple, List[AbstractUdsPacket]] +"""Typing alias of a sequence filled with :class:`~uds.messages.uds_packet.AbstractUdsPacket` instances.""" + PacketsRecordsTuple = Tuple[AbstractUdsPacketRecord, ...] -"""Typing alias of a tuple filled with UDS Packets.""" +"""Typing alias of a tuple filled with :class:`~uds.messages.uds_packet.AbstractUdsPacketRecord` instances.""" PacketsRecordsSequence = Union[PacketsRecordsTuple, List[AbstractUdsPacketRecord]] -"""Typing alias of a sequence filled with UDS Packets.""" +"""Typing alias of a sequence filled with :class:`~uds.messages.uds_packet.AbstractUdsPacketRecord` instances.""" + +PacketTyping = Union[AbstractUdsPacket, AbstractUdsPacketRecord] +"""Typing alias of UDS packet.""" +PacketsTuple = Union[PacketsDefinitionTuple, PacketsRecordsTuple] # noqa: F841 +"""Typing alias of a tuple filled with UDS packets.""" +PacketsSequence = Union[PacketsDefinitionSequence, PacketsRecordsSequence] +"""Typing alias of a sequence filled with UDS packets.""" diff --git a/uds/segmentation/__init__.py b/uds/segmentation/__init__.py new file mode 100644 index 00000000..32ba2d88 --- /dev/null +++ b/uds/segmentation/__init__.py @@ -0,0 +1,8 @@ +""" +A subpackage with tools for executing :ref:`segmentation `. + +It defines: + - common API interface for all segmentation duties +""" + +from .abstract_segmenter import SegmentationError, AbstractSegmenter diff --git a/uds/segmentation/abstract_segmenter.py b/uds/segmentation/abstract_segmenter.py new file mode 100644 index 00000000..ae607a71 --- /dev/null +++ b/uds/segmentation/abstract_segmenter.py @@ -0,0 +1,155 @@ +"""Definition of API for segmentation and desegmentation strategies.""" + +__all__ = ["SegmentationError", "AbstractSegmenter"] + +from typing import Tuple, Union, Any +from abc import ABC, abstractmethod + +from uds.messages import UdsMessage, UdsMessageRecord, \ + PacketTyping, PacketsSequence, PacketsDefinitionTuple, PacketTypesTuple + + +class SegmentationError(ValueError): + """UDS segmentation or desegmentation process cannot be completed due to input data inconsistency.""" + + +class AbstractSegmenter(ABC): + """ + Abstract definition of a segmenter class. + + Segmenter classes defines UDS segmentation and desegmentation + `strategies `_. + They contain helper methods that are essential for successful segmentation and desegmentation execution. + Each concrete segmenter class handles a single bus. + """ + + @property + @abstractmethod + def supported_packet_classes(self) -> Tuple[type]: + """Classes that define packet objects supported by this segmenter.""" + + @property + @abstractmethod + def initial_packet_types(self) -> PacketTypesTuple: + """Types of packets that initiates a diagnostic message transmission for the managed bus.""" + + def is_supported_packet(self, value: Any) -> bool: + """ + Check if the argument value is a packet object of a supported type. + + :param value: Value to check. + + :return: True if provided value is an object of a supported packet type, False otherwise. + """ + return isinstance(value, self.supported_packet_classes) + + def is_supported_packets_sequence(self, value: Any) -> bool: + """ + Check if the argument value is a packet sequence of a supported type. + + :param value: Value to check. + + :return: True if provided value is a packet sequence of a supported type, False otherwise. + """ + if not isinstance(value, (list, tuple)): + # not a sequence + return False + if not all(self.is_supported_packet(element) for element in value): + # at least one element is not a packet of a supported type + return False + # check if all packets are the same type + return len({type(element) for element in value}) == 1 + + def is_initial_packet(self, packet: PacketTyping) -> bool: + """ + Check whether a provided packet initiates a diagnostic message. + + :param packet: Packet to check. + + :raise TypeError: Provided value is not an object of a supported packet type. + + :return: True if the packet is the only or the first packet of a diagnostic message. + """ + if not self.is_supported_packet(packet): + raise TypeError(f"Provided value is not a packet object that is supported by this Segmenter. " + f"Actual value: {packet}.") + return packet.packet_type in self.initial_packet_types + + @abstractmethod + def get_consecutive_packets_number(self, first_packet: PacketTyping) -> int: # type: ignore + """ + Get number of consecutive packets that must follow this packet to fully store a diagnostic message. + + :param first_packet: The first packet of a segmented diagnostic message. + + :raise ValueError: Provided value is not an an initial packet. + + :return: Number of following packets that together carry a diagnostic message. + """ + if not self.is_initial_packet(first_packet): + raise ValueError(f"Provided value is not an initial packet of a type supported by this Segmenter. " + f"Actual value: {first_packet}.") + + @abstractmethod + def is_following_packets_sequence(self, packets: PacketsSequence) -> bool: # noqa + """ + Check whether provided packets are a sequence of following packets. + + Note: This function will return True under following conditions: + - a sequence of packets was provided + - the first packet in the sequence is an initial packet + - no other packet in the sequence is an initial packet + - each packet (except the first one) is a consecutive packet for the previous packet in the sequence + + :param packets: Packets sequence to check. + + :raise ValueError: Provided value is not a packets sequence of a supported type. + + :return: True if the provided packets are a sequence of following packets, otherwise False. + """ + if not self.is_supported_packets_sequence(packets): + raise ValueError(f"Provided value is not a packets sequence of a supported type." + f"Actual value: {packets}.") + if not self.is_initial_packet(packets[0]): + return False + + def is_complete_packets_sequence(self, packets: PacketsSequence) -> bool: + """ + Check whether provided packets are full sequence of packets that form exactly one diagnostic message. + + :param packets: Packets sequence to check. + + :return: True if the packets form exactly one diagnostic message. + False if there are missing, additional or inconsistent (e.g. two packets that initiate a message) packets. + """ + return self.is_following_packets_sequence(packets) and \ + self.get_consecutive_packets_number(packets[0]) == len(packets) + + @abstractmethod + def segmentation(self, message: UdsMessage) -> PacketsDefinitionTuple: # type: ignore + """ + Perform segmentation of a diagnostic message. + + :param message: UDS message to divide into UDS packets. + + :raise TypeError: Provided 'message' argument is not :class:`~uds.messages.uds_message.UdsMessage` type. + + :return: UDS packets that are an outcome of UDS message segmentation. + """ + if not isinstance(message, UdsMessage): + raise TypeError(f"Provided value is not UdsMessage type. Actual value: {message}.") + + @abstractmethod + def desegmentation(self, packets: PacketsSequence) -> Union[UdsMessage, UdsMessageRecord]: # type: ignore + """ + Perform desegmentation of UDS packets. + + :param packets: UDS packets to desegment into UDS message. + + :raise SegmentationError: Provided packets are not a complete packet sequence that form a diagnostic message. + + :return: A diagnostic message that is an outcome of UDS packets desegmentation. + """ + if not self.is_complete_packets_sequence(packets): + raise SegmentationError(f"Provided packets are not a complete that form a diagnostic message. " + f"Actual value: {packets}.")