diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 79c4c0f68..90ed3fe0d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,7 +7,6 @@ updates: interval: "monthly" reviewers: - "sebastianpfischer" - - "BKaDamien" - "Pog3k" - "sebclrsn" diff --git a/ci/Dockerfile b/ci/Dockerfile deleted file mode 100644 index 6f42c3996..000000000 --- a/ci/Dockerfile +++ /dev/null @@ -1,105 +0,0 @@ -# Get latest debian (>=11) -FROM debian:12 - -# Zephyr arguments -ARG ZSDK_VERSION=0.14.2 -ARG WGET_ARGS="-q --show-progress --progress=bar:force:noscroll --no-check-certificate" -ARG HOSTTYPE=x86_64 -ARG CMAKE_VERSION=3.20.5 - -# Set the working directory to /app -WORKDIR /kiso-project -# Define environment variable -ENV NAME kiso-container -# Update package management and install necessary packages -RUN apt-get update && apt-get install -y \ - g++ \ - gcc \ - make \ - gcc-multilib \ - g++-multilib \ - libmagic1 \ - git \ - graphviz \ - lcov \ - python3 \ - python3-pip \ - python3-dev \ - python3-setuptools \ - python3-wheel \ - ninja-build \ - gperf \ - ccache \ - dfu-util \ - device-tree-compiler \ - wget \ - xz-utils \ - file \ - zlib1g-dev \ - libffi-dev \ - libssl-dev \ - libbz2-dev \ - libreadline-dev \ - libsqlite3-dev \ - liblzma-dev \ - && rm -rf /var/lib/apt/lists/* - -# kiso vsc plugin dependencies -# DL3047 wants the --process flag to be set. The flag is already set in the WGET_ARGS. -# hadolint ignore=DL3047 -RUN wget ${WGET_ARGS} -qO- https://deb.nodesource.com/setup_15.x| bash - -RUN apt-get update && apt-get -y install nodejs \ - gnupg \ - libxshmfence1 \ - libglu1 \ - libasound2 \ - libgbm1 \ - libgtk-3-0 \ - libnss3 \ - xvfb \ - && rm -rf /var/lib/apt/lists/* -RUN npm install -g typescript -RUN npm install -g vsce - - -# Install CMake (for Zephyr) -# DL3047 wants the --process flag to be set. The flag is already set in the WGET_ARGS. -# hadolint ignore=DL3047 -RUN wget ${WGET_ARGS} https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-Linux-${HOSTTYPE}.sh && \ - chmod +x cmake-${CMAKE_VERSION}-Linux-${HOSTTYPE}.sh && \ - ./cmake-${CMAKE_VERSION}-Linux-${HOSTTYPE}.sh --skip-license --prefix=/usr/local && \ - rm -f ./cmake-${CMAKE_VERSION}-Linux-${HOSTTYPE}.sh - -# Install Zephyr SDK -# DL3047 wants the --process flag to be set. The flag is already set in the WGET_ARGS. -# hadolint ignore=DL3047 -RUN mkdir -p /opt/toolchains && \ - cd /opt/toolchains && \ - wget ${WGET_ARGS} https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v${ZSDK_VERSION}/zephyr-sdk-${ZSDK_VERSION}_linux-${HOSTTYPE}.tar.gz && \ - tar xf zephyr-sdk-${ZSDK_VERSION}_linux-${HOSTTYPE}.tar.gz && \ - zephyr-sdk-${ZSDK_VERSION}/setup.sh -t all -h -c && \ - rm zephyr-sdk-${ZSDK_VERSION}_linux-${HOSTTYPE}.tar.gz - -# Environment settings -ENV HOME=/home/kiso - -# Install pyenv -# DL3047 wants the --process flag to be set. The flag is already set in the WGET_ARGS. -# hadolint ignore=DL3047 -RUN wget ${WGET_ARGS} -qO- https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - -ENV PYENV_ROOT $HOME/.pyenv -ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH - -# Pre install python version for python TOX -RUN pyenv install 3.7 \ - && pyenv install 3.8 \ - && pyenv install 3.9 \ - && pyenv install 3.10 - -RUN /opt/toolchains/zephyr-sdk-${ZSDK_VERSION}/setup.sh -c - -# Install python packages. pip cache disabled to reduce docker image size. -RUN python3 -m pip install --no-cache-dir poetry tox west \ - && chmod -R 777 ${HOME} -ENV PATH="/home/kiso/.local/bin:${PATH}" diff --git a/docs/getting_started/pykiso_as_simulator.rst b/docs/getting_started/pykiso_as_simulator.rst new file mode 100644 index 000000000..c779555ba --- /dev/null +++ b/docs/getting_started/pykiso_as_simulator.rst @@ -0,0 +1,56 @@ +.. _pykiso_as_simulator: + +** NEW ** Pykiso as a simulator +------------------------------- + +Introduction +~~~~~~~~~~~~ + +Pykiso consists of two main components: + +* The testing framework +* The test environment creation and management + +Together, these components enable embedded software engineers to efficiently test their software in a familiar manner. +Pykiso's testing framework adopts an approach similar to popular embedded testing frameworks, +such as Google Test, making it intuitive for engineers with experience in embedded systems. + +Over the years, we have identified two main groups of Pykiso users: + +* Those who embrace our opinionated approach to testing embedded software. +* Those who appreciate the core principles and resources but prefer a different approach to testing. + +By decoupling the testing framework from the test environment creation, +Pykiso now caters to both groups, offering flexibility while retaining the benefits of its robust structure. + + + +Workflow Overview +~~~~~~~~~~~~~~~~~ + +**Create a test environment** +Begin by defining your test environment in a configuration file. (Refer to :ref:`basic_config_file` for more details.) + +**Strip the test suites section** +If needed, you can remove the `Test Suites` section from the configuration file to simplify the setup. + +**Write your (test) script** +Create a Python script, import the Pykiso library, import the test environment and use the auxiliaries to interact with the system under test. + + +This workflow allows users to leverage Pykiso's testing capabilities while maintaining flexibility in how they define and manage their test environments. + + + +Example +~~~~~~~ + +Definition of the test environment: + +.. literalinclude:: ../../examples/next_pykiso2/pykiso_as_simulator/serial.yaml + :language: yaml + +Creation of the test script: + +.. literalinclude:: ../../examples/next_pykiso2/pykiso_as_simulator/serial_simulation.py + :language: python diff --git a/docs/getting_started/user_guide.rst b/docs/getting_started/user_guide.rst index 2700b0c38..f310bb8ba 100644 --- a/docs/getting_started/user_guide.rst +++ b/docs/getting_started/user_guide.rst @@ -4,3 +4,5 @@ .. include:: basic_config_file.rst .. include:: basic_tests.rst + +.. include:: pykiso_as_simulator.rst diff --git a/docs/whats_new/version_ongoing.rst b/docs/whats_new/version_ongoing.rst index 05712fc66..265c3bb40 100644 --- a/docs/whats_new/version_ongoing.rst +++ b/docs/whats_new/version_ongoing.rst @@ -15,7 +15,16 @@ By default the CCPCanCan will use the trace size define in during the initialisa The log path is now initialise if set at None. + Results can be exported to Xray ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ see :ref:`xray` + +pykiso modules detachment +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The pykiso modules are now detached from the main testing framework. +This enable users to define their hw setup and load it in python. The auxiliaries +can now be used in a more flexible way in python. +See :ref:`pykiso_as_simulator` for more details. diff --git a/examples/next_pykiso2/pykiso_as_simulator/serial.yaml b/examples/next_pykiso2/pykiso_as_simulator/serial.yaml new file mode 100644 index 000000000..3e9a140f4 --- /dev/null +++ b/examples/next_pykiso2/pykiso_as_simulator/serial.yaml @@ -0,0 +1,12 @@ +auxiliaries: + com_aux_sender: + connectors: + com: loopback + type: pykiso.lib.auxiliaries.communication_auxiliary:CommunicationAuxiliary + com_aux_receiver: + connectors: + com: loopback + type: pykiso.lib.auxiliaries.communication_auxiliary:CommunicationAuxiliary +connectors: + loopback: + type: pykiso.lib.connectors.cc_raw_loopback:CCLoopback diff --git a/examples/next_pykiso2/pykiso_as_simulator/serial_simulation.py b/examples/next_pykiso2/pykiso_as_simulator/serial_simulation.py new file mode 100644 index 000000000..3ec230f03 --- /dev/null +++ b/examples/next_pykiso2/pykiso_as_simulator/serial_simulation.py @@ -0,0 +1,46 @@ +from pathlib import Path + +# Import pykiso +import pykiso + +# Load the test environment configuration +pykiso.load_config(Path(__file__).parent.resolve() / "serial.yaml") + +# From the pykiso library, import the type of auxiliary you defined in the configuration +# Here, it would be the CommunicationAuxiliary class +from pykiso.lib.auxiliaries.communication_auxiliary import CommunicationAuxiliary + + +def first_test(): + """Ping-pong test between sender and receiver (with no context manager) + """ + # Get the instance of the sender and receiver defined in the configuration + sender = CommunicationAuxiliary.get_instance('com_aux_sender') + receiver = CommunicationAuxiliary.get_instance('com_aux_receiver') + # Start the sender and receiver + sender.start() + receiver.start() + # Use the auxiliaries for my test + sender.send_message("Hello, World!") + assert receiver.receive_message(timeout_in_s = 2) == "Hello, World!" + print("Test passed!") + # Stop the sender and receiver + sender.stop() + receiver.stop() + +def second_test(): + """Ping-pong test between sender and receiver (with context manager) + """ + # Get the instance of the sender and receiver defined in the configuration + sender = CommunicationAuxiliary.get_instance('com_aux_sender') + receiver = CommunicationAuxiliary.get_instance('com_aux_receiver') + # Start the auxiliaries with a context manager + with sender as sender, receiver as receiver: + # Use the auxiliaries for my test + sender.send_message("Hello, World!") + assert receiver.receive_message(timeout_in_s = 2) == "Hello, World!" + print("Second test passed!") + +if __name__ == "__main__": + first_test() + second_test() diff --git a/src/pykiso/__init__.py b/src/pykiso/__init__.py index a80f6da4e..88935712d 100644 --- a/src/pykiso/__init__.py +++ b/src/pykiso/__init__.py @@ -45,3 +45,17 @@ ) logging_initializer.add_internal_log_levels() + +# Experimental - load configuration and create auxiliaries +from .config_parser import parse_config +from .test_setup.config_registry import ConfigRegistry + + +def load_config(config_file: str): + """Enable any user to load a pykiso yaml file from any script + + :param config_file: path to the pykiso yaml file + + """ + cfg = parse_config(config_file) + ConfigRegistry.register_aux_con(cfg) diff --git a/src/pykiso/auxiliary.py b/src/pykiso/auxiliary.py index 66bd52630..d74242657 100644 --- a/src/pykiso/auxiliary.py +++ b/src/pykiso/auxiliary.py @@ -24,7 +24,9 @@ import queue import threading from enum import Enum, unique -from typing import Any, Callable, List, Optional +from typing import Any, Callable, List, Optional, Self + +from pykiso.test_setup.config_registry import ConfigRegistry from .exceptions import AuxiliaryCreationError, AuxiliaryNotStarted from .logging_initializer import add_internal_log_levels, initialize_loggers @@ -48,6 +50,15 @@ class AuxiliaryInterface(abc.ABC): for the reception and one for the transmmission. """ + @classmethod + def get_instance(cls, name: str) -> Self: + """Experimental - Get an auxiliary instance by its name.""" + auxiliary = ConfigRegistry.get_aux_by_alias(name) + # Verify if the auxiliary is of the right type + if not isinstance(auxiliary, cls): + raise ValueError(f"Requested auxiliary {name} is not of type {cls}") + return auxiliary + def __init__( self, name: str = None, @@ -201,7 +212,8 @@ def _start_tx_task(self) -> None: task_name = f"{self.name}_tx" log.internal_debug("start transmit task %s", task_name) - self.tx_thread = threading.Thread(name=task_name, target=self._transmit_task) + # Any created thread should disappear after main-thread exit + self.tx_thread = threading.Thread(name=task_name, target=self._transmit_task, daemon=True) self.tx_thread.start() def _start_rx_task(self) -> None: @@ -212,7 +224,8 @@ def _start_rx_task(self) -> None: with self.rx_lock: task_name = f"{self.name}_rx" log.internal_debug("start reception task %s", task_name) - self.rx_thread = threading.Thread(name=task_name, target=self._reception_task) + # Any created thread should disappear after main-thread exit + self.rx_thread = threading.Thread(name=task_name, target=self._reception_task, daemon=True) self.rx_thread.start() def _stop_tx_task(self) -> None: @@ -257,6 +270,21 @@ def stop(self) -> bool: """ return self.delete_instance() + def __enter__(self) -> Self: + """Context manager entry point""" + if self.start(): + return self + else: + raise AuxiliaryNotStarted(f"Failed to start auxiliary {self.name}") + + def __exit__(self, type, value, traceback): + """Context manager exit point""" + stop_status = self.stop() + if traceback: + log.error(f"Error occurred during auxiliary {self.name} execution: {type=}, {value=}, {traceback=}") + if not stop_status: + raise RuntimeError(f"Failed to stop auxiliary {self.name}") + def suspend(self) -> bool: """Supend current auxiliary's run. diff --git a/src/pykiso/lib/auxiliaries/proxy_auxiliary.py b/src/pykiso/lib/auxiliaries/proxy_auxiliary.py index 82ac2c7d5..48938b3f4 100644 --- a/src/pykiso/lib/auxiliaries/proxy_auxiliary.py +++ b/src/pykiso/lib/auxiliaries/proxy_auxiliary.py @@ -357,7 +357,7 @@ def _receive_message(self, timeout_in_s: float = 0) -> None: if received_data is not None: self.logger.debug( "received response : data %s || channel : %s", - received_data.hex(), + received_data, self.channel.name, ) for conn in self.proxy_channels: diff --git a/src/pykiso/lib/connectors/cc_raw_loopback.py b/src/pykiso/lib/connectors/cc_raw_loopback.py index a6a30f61c..ed41715fa 100644 --- a/src/pykiso/lib/connectors/cc_raw_loopback.py +++ b/src/pykiso/lib/connectors/cc_raw_loopback.py @@ -19,6 +19,7 @@ """ import threading +import time from collections import deque from typing import Dict, Optional @@ -62,8 +63,12 @@ def _cc_receive(self, timeout: float) -> Dict[str, Optional[bytes]]: :return: dictionary containing the received bytes if successful, otherwise None """ with self.lock: - try: - recv_msg = self._loopback_buffer.popleft() - return {"msg": recv_msg} - except IndexError: - return {"msg": None} + # Simulate a blocking on receive + start = time.time_ns() + while (time.time_ns() - start) < timeout * 1e9: + try: + recv_msg = self._loopback_buffer.popleft() + return {"msg": recv_msg} + except IndexError: + time.sleep(0.1) + return {"msg": None} diff --git a/tests/dummy_serial.yaml b/tests/dummy_serial.yaml new file mode 100644 index 000000000..3e9a140f4 --- /dev/null +++ b/tests/dummy_serial.yaml @@ -0,0 +1,12 @@ +auxiliaries: + com_aux_sender: + connectors: + com: loopback + type: pykiso.lib.auxiliaries.communication_auxiliary:CommunicationAuxiliary + com_aux_receiver: + connectors: + com: loopback + type: pykiso.lib.auxiliaries.communication_auxiliary:CommunicationAuxiliary +connectors: + loopback: + type: pykiso.lib.connectors.cc_raw_loopback:CCLoopback diff --git a/tests/test_dt_auxiliary_interface.py b/tests/test_dt_auxiliary_interface.py index caf5266e8..274f794e4 100644 --- a/tests/test_dt_auxiliary_interface.py +++ b/tests/test_dt_auxiliary_interface.py @@ -8,6 +8,7 @@ ########################################################################## import logging +from unittest.mock import Mock import pytest @@ -229,6 +230,38 @@ def test_stop(mocker, aux_inst): assert state is True +def test_aux_as_context_manager(mocker, aux_inst): + create_inst_mock: Mock = mocker.patch.object(aux_inst, "create_instance", return_value=True) + delete_inst_mock: Mock = mocker.patch.object(aux_inst, "delete_instance", return_value=True) + + with aux_inst: + pass + + create_inst_mock.assert_called_once() + delete_inst_mock.assert_called_once() + +def test_context_manager_with_errors(mocker, aux_inst, caplog): + mocker.patch.object(aux_inst, "create_instance", return_value=False) + # First test with failing start + with pytest.raises(AuxiliaryNotStarted): + with aux_inst: + pass + + mocker.patch.object(aux_inst, "create_instance", return_value=True) + mocker.patch.object(aux_inst, "delete_instance", return_value=False) + # Second test with failing stop + with pytest.raises(RuntimeError): + with aux_inst: + pass + + mocker.patch.object(aux_inst, "delete_instance", return_value=True) + # Last test: Check if the failure in the context was recorded + with pytest.raises(ValueError): + with aux_inst: + raise ValueError("Something went wrong") + assert "Something went wrong" in caplog.text + + def test_suspend(mocker, aux_inst): mocker.patch.object(aux_inst, "delete_instance", return_value=True) diff --git a/tests/test_pykiso_as_simulator.py b/tests/test_pykiso_as_simulator.py new file mode 100644 index 000000000..636a63b15 --- /dev/null +++ b/tests/test_pykiso_as_simulator.py @@ -0,0 +1,33 @@ +from pathlib import Path + +import pytest + +import pykiso +from pykiso.lib.auxiliaries.acroname_auxiliary import AcronameAuxiliary +from pykiso.lib.auxiliaries.communication_auxiliary import CommunicationAuxiliary + + +def test_verify_pykiso_2_import_mechanism(): + # Load verification + pykiso.load_config(Path(__file__).parent.resolve() / "dummy_serial.yaml") + sender = CommunicationAuxiliary.get_instance('com_aux_sender') + receiver = CommunicationAuxiliary.get_instance('com_aux_receiver') + # Content verification + with sender as sender, receiver as receiver: + sender.send_message("Hello, World!") + assert receiver.receive_message() == "Hello, World!" + # Cleanup + pykiso.ConfigRegistry.delete_aux_con() + + +def test_verify_pykiso_2_get_no_instance(): + pykiso.load_config(Path(__file__).parent.resolve() / "dummy_serial.yaml") + assert CommunicationAuxiliary.get_instance('com_aux_sender') != None + # No existing instance + with pytest.raises(ValueError): + CommunicationAuxiliary.get_instance('com_aux_receiver2') + # Instance with the wrong type + with pytest.raises(ValueError): + AcronameAuxiliary.get_instance('com_aux_receiver') + # Cleanup + pykiso.ConfigRegistry.delete_aux_con() diff --git a/tests/test_record_auxiliary.py b/tests/test_record_auxiliary.py index 994bac13f..51f23c438 100644 --- a/tests/test_record_auxiliary.py +++ b/tests/test_record_auxiliary.py @@ -10,11 +10,10 @@ import builtins import logging import pathlib -import threading import pytest -from pykiso.lib.auxiliaries.record_auxiliary import RecordAuxiliary, StringIOHandler +from pykiso.lib.auxiliaries.record_auxiliary import RecordAuxiliary, StringIOHandler, threading @pytest.fixture @@ -134,9 +133,7 @@ def test_delete_aux_error(caplog, mocker, mock_channel): ], ) def test_receive(data, expected_data, mocker, mock_channel): - event_mock = mocker.patch.object( - threading.Event, "is_set", side_effect=[False, True] - ) + event_mock = mocker.patch("pykiso.lib.auxiliaries.record_auxiliary.threading.Event.is_set", side_effect=[False, True]) mocker.patch.object(threading.Thread, "start", return_value=None) record_aux = RecordAuxiliary(mock_channel, is_active=True)