diff --git a/docs/advanced_usage/advanced_config_file.rst b/docs/advanced_usage/advanced_config_file.rst index 00041bf6..3c17ed19 100644 --- a/docs/advanced_usage/advanced_config_file.rst +++ b/docs/advanced_usage/advanced_config_file.rst @@ -131,6 +131,12 @@ that will have exclusive access to the communication channel. A the created :py:class:`~pykiso.lib.auxiliaries.proxy_auxiliary.ProxyAuxiliary`. This allows to keep a minimalistic :py:class:`~pykiso.connector.CChannel` implementation. +Access to attributes and methods of the defined communication channel that has been +attached to the created :py:class:`~pykiso.lib.auxiliaries.proxy_auxiliary.ProxyAuxiliary` +is still possible, but keep in mind that if one auxiliary modifies one the communication +channel's attributes, every other auxiliary sharing this communication channel will be +affected by this change. + An illustration of the resulting internal setup can be found at :ref:`proxy_aux`. In other words, if you define the following YAML configuration file: diff --git a/docs/whats_new/version_ongoing.rst b/docs/whats_new/version_ongoing.rst index 16982e2d..dbb78560 100644 --- a/docs/whats_new/version_ongoing.rst +++ b/docs/whats_new/version_ongoing.rst @@ -9,3 +9,13 @@ specify their names for each defined auxiliary. This is no longer the case and specifying them in only one auxiliary will be enough for the loggers to stay enabled. + + +Internal creation of proxy auxiliaries +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is no longer necessary to manually defined a ``ProxyAuxiliary`` with +``CCProxy``s yourself. If you simply pass the communication channel to +each auxiliary that has to share it, ``pykiso`` will do the rest for you. + + diff --git a/src/pykiso/auxiliary.py b/src/pykiso/auxiliary.py index c9cbb0e1..d100b93d 100644 --- a/src/pykiso/auxiliary.py +++ b/src/pykiso/auxiliary.py @@ -26,9 +26,9 @@ import time from typing import Any -from .logging_initializer import add_logging_level -from .test_setup.config_registry import ConfigRegistry -from .types import MsgType +from pykiso.logging_initializer import add_internal_log_levels +from pykiso.test_setup.config_registry import ConfigRegistry +from pykiso.types import MsgType log = logging.getLogger(__name__) @@ -38,18 +38,9 @@ class AuxiliaryCommon(metaclass=abc.ABCMeta): multiprocessing and thread auxiliary interface. """ - def __new__(cls: AuxiliaryCommon, *args, **kwargs) -> AuxiliaryCommon: - """Create instance and add internal kiso log levels in - case the auxiliary is used outside the pykiso context - """ - if not hasattr(logging, "INTERNAL_WARNING"): - add_logging_level("INTERNAL_WARNING", logging.WARNING + 1) - add_logging_level("INTERNAL_INFO", logging.INFO + 1) - add_logging_level("INTERNAL_DEBUG", logging.DEBUG + 1) - return super(AuxiliaryCommon, cls).__new__(cls) - def __init__(self) -> None: """Auxiliary common attributes initialization.""" + add_internal_log_levels() self.name = None self.queue_in = None self.lock = None diff --git a/src/pykiso/interfaces/dt_auxiliary.py b/src/pykiso/interfaces/dt_auxiliary.py index 4f80e48b..5a863018 100644 --- a/src/pykiso/interfaces/dt_auxiliary.py +++ b/src/pykiso/interfaces/dt_auxiliary.py @@ -28,7 +28,7 @@ from typing import Any, Callable, List, Optional from ..exceptions import AuxiliaryCreationError -from ..logging_initializer import add_logging_level, initialize_loggers +from ..logging_initializer import add_internal_log_levels, initialize_loggers log = logging.getLogger(__name__) @@ -49,16 +49,6 @@ class DTAuxiliaryInterface(abc.ABC): for the reception and one for the transmmission. """ - def __new__(cls, *args, **kwargs): - """Create instance and add internal kiso log levels in - case the auxiliary is used outside the pykiso context - """ - if not hasattr(logging, "INTERNAL_WARNING"): - add_logging_level("INTERNAL_WARNING", logging.WARNING + 1) - add_logging_level("INTERNAL_INFO", logging.INFO + 1) - add_logging_level("INTERNAL_DEBUG", logging.DEBUG + 1) - return super(DTAuxiliaryInterface, cls).__new__(cls) - def __init__( self, name: str = None, @@ -82,10 +72,11 @@ def __init__( :param auto_start: determine if the auxiliayry is automatically started (magic import) or manually (by user) """ + initialize_loggers(activate_log) + add_internal_log_levels() self.name = name self.is_proxy_capable = is_proxy_capable self.auto_start = auto_start - initialize_loggers(activate_log) self.lock = threading.RLock() self.stop_tx = threading.Event() self.stop_rx = threading.Event() @@ -209,7 +200,7 @@ def _start_rx_task(self) -> None: log.internal_debug("reception task is not needed, don't start it") return - log.internal_debug(f"start reception task {self.name}_tx") + log.internal_debug(f"start reception task {self.name}_rx") self.rx_thread = threading.Thread( name=f"{self.name}_rx", target=self._reception_task ) diff --git a/src/pykiso/lib/auxiliaries/mp_proxy_auxiliary.py b/src/pykiso/lib/auxiliaries/mp_proxy_auxiliary.py index 67b5dd81..74970ad5 100644 --- a/src/pykiso/lib/auxiliaries/mp_proxy_auxiliary.py +++ b/src/pykiso/lib/auxiliaries/mp_proxy_auxiliary.py @@ -57,6 +57,7 @@ from typing import List, Optional, Tuple from pykiso import AuxiliaryInterface, CChannel, MpAuxiliaryInterface +from pykiso.lib.connectors.cc_mp_proxy import CCMpProxy from pykiso.test_setup.config_registry import ConfigRegistry from pykiso.test_setup.dynamic_loader import PACKAGE @@ -95,13 +96,13 @@ def __init__( activate=activate_trace, dir=trace_dir, name=trace_name ) self.proxy_channels = self.get_proxy_con(aux_list) - self.logger = None + self.logger: logging.Logger = None self.aux_list = aux_list super().__init__(**kwargs) def _init_trace( self, - logger, + logger: logging.Logger, activate: bool, t_dir: Optional[str] = None, t_name: Optional[str] = None, @@ -144,7 +145,7 @@ def _init_trace( handler.setLevel(logging.DEBUG) logger.addHandler(handler) - def get_proxy_con(self, aux_list: List[str]) -> Tuple[AuxiliaryInterface]: + def get_proxy_con(self, aux_list: List[str]) -> Tuple[CCMpProxy]: """Retrieve all connector associated to all given existing Auxiliaries. If auxiliary alias exists but auxiliary instance was not created @@ -155,7 +156,7 @@ def get_proxy_con(self, aux_list: List[str]) -> Tuple[AuxiliaryInterface]: :return: tuple containing all connectors associated to all given auxiliaries """ - channel_inst = [] + channel_inst: List[CCMpProxy] = [] for aux_name in aux_list: aux_inst = sys.modules.get(f"{PACKAGE}.auxiliaries.{aux_name}") @@ -177,6 +178,14 @@ def get_proxy_con(self, aux_list: List[str]) -> Tuple[AuxiliaryInterface]: else: log.error(f"Auxiliary : {aux_name} doesn't exist") + # Check if auxes/connectors are compatible with the proxy aux + self._check_channels_compatibility(channel_inst) + + # Finally bind the physical channel to the proxy channels to + # share its API to the user's auxiliaries + for channel in channel_inst: + channel._bind_channel_info(self) + return tuple(channel_inst) @staticmethod @@ -192,6 +201,22 @@ def _check_compatibility(aux: AuxiliaryInterface) -> None: f"Auxiliary {aux} is not compatible with a proxy auxiliary" ) + @staticmethod + def _check_channels_compatibility(channels: List[CChannel]) -> None: + """Check if all associated channels are compatible. + + :param channels: all channels collected by the proxy aux + + :raises TypeError: if the connector is not an instance of + CCProxy + """ + for channel in channels: + if not isinstance(channel, CCMpProxy): + raise TypeError( + f"Channel {channel} is not compatible! " + f"Expected a CCMpProxy instance, got {channel.__class__.__name__}" + ) + def _create_auxiliary_instance(self) -> bool: """Open current associated channel. diff --git a/src/pykiso/lib/auxiliaries/proxy_auxiliary.py b/src/pykiso/lib/auxiliaries/proxy_auxiliary.py index ebd4a699..47193b79 100644 --- a/src/pykiso/lib/auxiliaries/proxy_auxiliary.py +++ b/src/pykiso/lib/auxiliaries/proxy_auxiliary.py @@ -144,7 +144,7 @@ def _init_trace( return logger - def get_proxy_con(self, aux_list: List[str]) -> Tuple: + def get_proxy_con(self, aux_list: List[str]) -> Tuple[CCProxy, ...]: """Retrieve all connector associated to all given existing Auxiliaries. If auxiliary alias exists but auxiliary instance was not created @@ -155,7 +155,7 @@ def get_proxy_con(self, aux_list: List[str]) -> Tuple: :return: tuple containing all connectors associated to all given auxiliaries """ - channel_inst = [] + channel_inst: List[CCProxy] = [] for aux in aux_list: # aux_list can contain a auxiliary instance just grab the @@ -184,10 +184,15 @@ def get_proxy_con(self, aux_list: List[str]) -> Tuple: # invalid one else: log.error(f"Auxiliary '{aux}' doesn't exist") - # Finally just check if auxes/connectors are compatible with - # the proxy aux + + # Check if auxes/connectors are compatible with the proxy aux self._check_channels_compatibility(channel_inst) + # Finally bind the physical channel to the proxy channels to + # share its API to the user's auxiliaries + for channel in channel_inst: + channel._bind_channel_info(self) + return tuple(channel_inst) @staticmethod diff --git a/src/pykiso/lib/connectors/cc_mp_proxy.py b/src/pykiso/lib/connectors/cc_mp_proxy.py index eb70e6a6..568a31b2 100644 --- a/src/pykiso/lib/connectors/cc_mp_proxy.py +++ b/src/pykiso/lib/connectors/cc_mp_proxy.py @@ -22,17 +22,17 @@ .. currentmodule:: cc_mp_proxy """ - from __future__ import annotations import logging import multiprocessing import queue -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from pykiso.connector import CChannel if TYPE_CHECKING: + from pykiso.lib.auxiliaries.mp_proxy_auxiliary import MpProxyAuxiliary from pykiso.types import ProxyReturn log = logging.getLogger(__name__) @@ -43,11 +43,53 @@ class CCMpProxy(CChannel): def __init__(self, **kwargs): """Initialize attributes.""" + kwargs.update(processing=True) super().__init__(**kwargs) # instantiate directly both queue_in and queue_out self.queue_in = multiprocessing.Queue() self.queue_out = multiprocessing.Queue() self.timeout = 1 + # used for physical channel attributes access from main auxiliary + self._proxy = None + self._physical_channel = None + + def _bind_channel_info(self, proxy_aux: MpProxyAuxiliary): + """Bind a :py:class:`~pykiso.lib.auxiliaries.mp_proxy_auxiliary.MpProxyAuxiliary` + instance that is instanciated in order to handle the connection of + multiple auxiliaries to a single communication channel in order to + hide the underlying proxy setup. + + :param proxy_aux: the proxy auxiliary instance that is holding the + real communication channel. + """ + self._proxy = proxy_aux + self._physical_channel = proxy_aux.channel + + def __getattr__(self, name: str) -> Any: + """Implement getattr to retrieve attributes from the real channel attached + to the underlying :py:class:`~pykiso.lib.auxiliaries.mp_proxy_auxiliary.MpProxyAuxiliary`. + + :param name: name of the attribute to get. + :raises AttributeError: if the attribute is not part of the real + channel instance or if the real channel hasn't been bound to + this proxy channel yet. + :return: the found attribute value. + """ + if self._physical_channel is not None: + with self._proxy.lock: + return getattr(self._physical_channel, name) + raise AttributeError( + f"{self.__class__.__name__} object has no attribute '{name}'" + ) + + def __getstate__(self): + """Avoid getattr to be called on pickling before instanciation, + which would cause infinite recursion. + """ + return self.__dict__ + + def __setstate__(self, state): + self.__dict__ = state def _cc_open(self) -> None: """Open proxy channel. diff --git a/src/pykiso/lib/connectors/cc_proxy.py b/src/pykiso/lib/connectors/cc_proxy.py index a70b59b6..e2541e62 100644 --- a/src/pykiso/lib/connectors/cc_proxy.py +++ b/src/pykiso/lib/connectors/cc_proxy.py @@ -32,6 +32,7 @@ from pykiso.connector import CChannel if TYPE_CHECKING: + from pykiso.lib.auxiliaries.proxy_auxiliary import ProxyAuxiliary from pykiso.types import ProxyReturn @@ -48,6 +49,39 @@ def __init__(self, **kwargs): self.timeout = 1 self._lock = threading.Lock() self._tx_callback = None + self._proxy = None + self._physical_channel = None + + def _bind_channel_info(self, proxy_aux: ProxyAuxiliary): + """Bind a :py:class:`~pykiso.lib.auxiliaries.proxy_auxiliary.ProxyAuxiliary` + instance that is instanciated in order to handle the connection of + multiple auxiliaries to a single communication channel. + + This allows to access the real communication channel's attributes + and hides the underlying proxy setup. + + :param proxy_aux: the proxy auxiliary instance that is holding the + real communication channel. + """ + self._proxy = proxy_aux + self._physical_channel = proxy_aux.channel + + def __getattr__(self, name: str) -> Any: + """Implement getattr to retrieve attributes from the real channel attached + to the underlying :py:class:`~pykiso.lib.auxiliaries.proxy_auxiliary.ProxyAuxiliary`. + + :param name: name of the attribute to get. + :raises AttributeError: if the attribute is not part of the real + channel instance or if the real channel hasn't been bound to + this proxy channel yet. + :return: the found attribute value. + """ + if self._physical_channel is not None: + with self._proxy.lock: + return getattr(self._physical_channel, name) + raise AttributeError( + f"{self.__class__.__name__} object has no attribute '{name}'" + ) def detach_tx_callback(self) -> None: """Detach the current callback.""" diff --git a/tests/conftest.py b/tests/conftest.py index 778f12c9..6d8ae01c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,19 +22,11 @@ from pykiso.lib.connectors import cc_example from pykiso.lib.connectors.cc_pcan_can import CCPCanCan from pykiso.lib.connectors.cc_vector_can import CCVectorCan -from pykiso.logging_initializer import ( - LogOptions, - add_logging_level, - get_logging_options, -) +from pykiso.logging_initializer import LogOptions, get_logging_options from pykiso.test_coordinator import test_case from pykiso.test_coordinator.test_case import define_test_parameters from pykiso.test_setup.dynamic_loader import DynamicImportLinker -# add the internal log levels to avoid errors during unit tests execution -add_logging_level("INTERNAL_WARNING", logging.WARNING + 1) -add_logging_level("INTERNAL_INFO", logging.INFO + 1) -add_logging_level("INTERNAL_DEBUG", logging.DEBUG + 1) ## skip slow test by default def pytest_addoption(parser): diff --git a/tests/test_auxiliary.py b/tests/test_auxiliary.py index 7578e266..ca8c733d 100644 --- a/tests/test_auxiliary.py +++ b/tests/test_auxiliary.py @@ -9,7 +9,6 @@ import logging import time -from multiprocessing.connection import deliver_challenge import pytest @@ -31,7 +30,7 @@ class MockAux(AuxiliaryInterface): def __init__(self, param_1=None, param_2=None, **kwargs): self.param_1 = param_1 self.param_2 = param_2 - super().__init__(**kwargs) + AuxiliaryInterface.__init__(self, **kwargs) _create_auxiliary_instance = mocker.stub(name="_create_auxiliary_instance") _delete_auxiliary_instance = mocker.stub(name="_delete_auxiliary_instance") @@ -56,7 +55,7 @@ class MockThreadAux(AuxiliaryInterface): def __init__(self, param_1=None, param_2=None, **kwargs): self.param_1 = param_1 self.param_2 = param_2 - super().__init__(**kwargs) + AuxiliaryInterface.__init__(self, **kwargs) _create_auxiliary_instance = mocker.stub(name="_create_auxiliary_instance") _delete_auxiliary_instance = mocker.stub(name="_delete_auxiliary_instance") @@ -76,7 +75,7 @@ def __init__(self, param_1=None, param_2=None, **kwargs): ) self.param_1 = param_1 self.param_2 = param_2 - super().__init__(name="mp_aux", **kwargs) + MpAuxiliaryInterface.__init__(self, name="mp_aux", **kwargs) _create_auxiliary_instance = mocker.stub(name="_create_auxiliary_instance") _delete_auxiliary_instance = mocker.stub(name="_delete_auxiliary_instance") @@ -91,7 +90,7 @@ def __init__(self, param_1=None, param_2=None, **kwargs): def mock_simple_aux(mocker): class MockSimpleAux(SimpleAuxiliaryInterface): def __init__(self, **kwargs): - super().__init__(**kwargs) + SimpleAuxiliaryInterface.__init__(self, **kwargs) _create_auxiliary_instance = mocker.stub(name="_create_auxiliary_instance") _delete_auxiliary_instance = mocker.stub(name="_delete_auxiliary_instance") diff --git a/tests/test_cc_mp_proxy.py b/tests/test_cc_mp_proxy.py index 1eb1aba4..4908efba 100644 --- a/tests/test_cc_mp_proxy.py +++ b/tests/test_cc_mp_proxy.py @@ -18,6 +18,12 @@ def test_constructor(): assert isinstance(con_inst.queue_in, type(multiprocessing.Queue())) assert isinstance(con_inst.queue_out, type(multiprocessing.Queue())) + # pickling quick test + assert con_inst.__getstate__() == con_inst.__dict__ + new_dict = {**con_inst.__dict__, **{"some_attr": 12}} + con_inst.__setstate__(new_dict) + assert con_inst.__getstate__() == new_dict + def test_queue_reference(): con_inst = CCMpProxy() diff --git a/tests/test_cc_process.py b/tests/test_cc_process.py index 14464fe8..34f32232 100644 --- a/tests/test_cc_process.py +++ b/tests/test_cc_process.py @@ -38,7 +38,7 @@ def test_process(mocker): # sleep 1s # print "hello" on stdout # print "pykiso" on stdout - 'import sys;import time;print(sys.stdin.readline().strip());sys.stdout.flush();time.sleep(1);print(\'error\', file=sys.stderr);sys.stderr.flush();time.sleep(1);print("hello");print("pykiso")', + 'import sys;import time;print(sys.stdin.readline().strip());sys.stdout.flush();time.sleep(0.1);print(\'error\', file=sys.stderr);sys.stderr.flush();time.sleep(0.1);print("hello");print("pykiso")', ], ) # Start the process @@ -48,15 +48,15 @@ def test_process(mocker): cc_process.start() # Receive nothing as process waits for input - assert cc_process.cc_receive(3) == {"msg": None} + assert cc_process.cc_receive(0.1) == {"msg": None} cc_process._cc_send("hi\r\n") - assert cc_process.cc_receive(3) == {"msg": {"stdout": "hi\n"}} - assert cc_process.cc_receive(3) == {"msg": {"stderr": "error\n"}} - assert cc_process.cc_receive(3) == {"msg": {"stdout": "hello\n"}} - assert cc_process.cc_receive(3) == {"msg": {"stdout": "pykiso\n"}} - assert cc_process.cc_receive(3) == {"msg": {"exit": 0}} + assert cc_process.cc_receive(1) == {"msg": {"stdout": "hi\n"}} + assert cc_process.cc_receive(1) == {"msg": {"stderr": "error\n"}} + assert cc_process.cc_receive(1) == {"msg": {"stdout": "hello\n"}} + assert cc_process.cc_receive(1) == {"msg": {"stdout": "pykiso\n"}} + assert cc_process.cc_receive(1) == {"msg": {"exit": 0}} cc_process._cc_close() - assert cc_process.cc_receive(3) == {"msg": None} + assert cc_process.cc_receive(0.1) == {"msg": None} def test_process_binary(mocker): @@ -82,7 +82,7 @@ def test_process_binary(mocker): # print "hello" on stdout # sleep 1s # print "pykiso" on stdout - 'import sys;import time;sys.stdout.write(sys.stdin.readline().strip());sys.stdout.flush();time.sleep(1);sys.stderr.write("error");sys.stderr.flush();time.sleep(1);sys.stdout.write("hello");sys.stdout.flush();time.sleep(1);sys.stdout.write("pykiso")', + 'import sys;import time;sys.stdout.write(sys.stdin.readline().strip());sys.stdout.flush();time.sleep(0.1);sys.stderr.write("error");sys.stderr.flush();time.sleep(0.1);sys.stdout.write("hello");sys.stdout.flush();time.sleep(0.1);sys.stdout.write("pykiso")', ], ) # Start the process @@ -92,13 +92,13 @@ def test_process_binary(mocker): cc_process._cc_send({"command": "start", "executable": "", "args": ""}) cc_process._cc_send(b"hi\n") - assert cc_process.cc_receive(3) == {"msg": {"stdout": b"hi"}} - assert cc_process.cc_receive(3) == {"msg": {"stderr": b"error"}} - assert cc_process.cc_receive(3) == {"msg": {"stdout": b"hello"}} - assert cc_process.cc_receive(3) == {"msg": {"stdout": b"pykiso"}} - assert cc_process.cc_receive(3) == {"msg": {"exit": 0}} + assert cc_process.cc_receive(1) == {"msg": {"stdout": b"hi"}} + assert cc_process.cc_receive(1) == {"msg": {"stderr": b"error"}} + assert cc_process.cc_receive(1) == {"msg": {"stdout": b"hello"}} + assert cc_process.cc_receive(1) == {"msg": {"stdout": b"pykiso"}} + assert cc_process.cc_receive(1) == {"msg": {"exit": 0}} cc_process._cc_close() - assert cc_process.cc_receive(3) == {"msg": None} + assert cc_process.cc_receive(0.1) == {"msg": None} def test_send_without_pipe_exception(mocker): diff --git a/tests/test_mp_proxy_auxiliary.py b/tests/test_mp_proxy_auxiliary.py index b4d63b21..a05cd4cd 100644 --- a/tests/test_mp_proxy_auxiliary.py +++ b/tests/test_mp_proxy_auxiliary.py @@ -6,6 +6,7 @@ # # SPDX-License-Identifier: EPL-2.0 ########################################################################## +import importlib import logging import queue import sys @@ -14,6 +15,7 @@ import pytest +import pykiso from pykiso.connector import CChannel from pykiso.interfaces.thread_auxiliary import AuxiliaryInterface from pykiso.lib.auxiliaries.mp_proxy_auxiliary import ( @@ -21,16 +23,22 @@ MpProxyAuxiliary, TraceOptions, ) +from pykiso.lib.connectors.cc_mp_proxy import CCMpProxy +from pykiso.lib.connectors.cc_proxy import CCProxy + +pykiso.logging_initializer.log_options = pykiso.logging_initializer.LogOptions( + ".", "INFO", "TEXT", False +) @pytest.fixture def mock_auxiliaries(mocker): - class MockProxyCChannel(CChannel): + class MockProxyCChannel(CCMpProxy): def __init__(self, name=None, *args, **kwargs): + super(MockProxyCChannel, self).__init__(*args, **kwargs) self.name = name - self.queue_in = queue.Queue() self.queue_out = queue.Queue() - super(MockProxyCChannel, self).__init__(*args, **kwargs) + self.queue_in = queue.Queue() _cc_open = mocker.stub(name="_cc_open") open = mocker.stub(name="open") @@ -161,7 +169,9 @@ def test_init_trace_not_activated(mocker, mp_proxy_auxiliary_inst): def test_get_proxy_con_pre_load(mocker, mp_proxy_auxiliary_inst, caplog): - mock_check_comp = mocker.patch.object(MpProxyAuxiliary, "_check_compatibility") + mock_check_comp = mocker.patch.object( + MpProxyAuxiliary, "_check_channels_compatibility" + ) mock_get_alias = mocker.patch.object( ConfigRegistry, "get_auxes_alias", return_value="later_aux" @@ -172,9 +182,13 @@ class Linker: def __init__(self): self._aux_cache = AuxCache() + class FakeCCMpProxy(CCMpProxy): + def _bind_channel_info(self, *args, **kwargs): + pass + class FakeAux: def __init__(self): - self.channel = True + self.channel = FakeCCMpProxy() self.is_proxy_capable = True class AuxCache: @@ -183,33 +197,64 @@ def get_instance(self, aux_name): ConfigRegistry._linker = Linker() - with caplog.at_level( - logging.WARNING, - ): - + with caplog.at_level(logging.WARNING): result_get_proxy = mp_proxy_auxiliary_inst.get_proxy_con(["later_aux"]) + assert ( "Auxiliary : later_aux is not using import magic mechanism (pre-loaded)" in caplog.text ) assert len(result_get_proxy) == 1 - assert isinstance(result_get_proxy[0], bool) + assert isinstance(result_get_proxy[0], FakeCCMpProxy) mock_check_comp.assert_called() mock_get_alias.get_called() -def test_get_proxy_con_valid_(mocker, mp_proxy_auxiliary_inst, mock_auxiliaries): +def test_get_proxy_con_valid(mocker, mp_proxy_auxiliary_inst, mock_auxiliaries): mock_check_comp = mocker.patch.object(MpProxyAuxiliary, "_check_compatibility") + mock_check_channel_comp = mocker.patch.object( + MpProxyAuxiliary, "_check_channels_compatibility" + ) AUX_LIST_NAMES = ["MockAux1", "MockAux2"] result_get_proxy = mp_proxy_auxiliary_inst.get_proxy_con(AUX_LIST_NAMES) - assert len(result_get_proxy) == 2 + assert len(result_get_proxy) == len(AUX_LIST_NAMES) assert all(isinstance(items, CChannel) for items in result_get_proxy) - mock_check_comp.assert_called() + mock_check_channel_comp.assert_called_once() + assert mock_check_comp.call_count == len(AUX_LIST_NAMES) + + +def test_get_proxy_con_invalid_cchannel(mocker, caplog, mp_proxy_auxiliary_inst): + mock_get_alias = mocker.patch.object( + ConfigRegistry, "get_auxes_alias", return_value="later_aux" + ) + mp_proxy_auxiliary_inst.aux_list = ["later_aux"] + + class Linker: + def __init__(self): + self._aux_cache = AuxCache() + + class OtherCChannel(CChannel): + def _bind_channel_info(self, *args, **kwargs): + pass + class FakeAux: + def __init__(self): + self.channel = OtherCChannel() + self.is_proxy_capable = True + + class AuxCache: + def get_instance(self, aux_name): + return FakeAux() -def test_get_proxy_con_invalid_(mocker, caplog, mp_proxy_auxiliary_inst): + ConfigRegistry._linker = Linker() + + with pytest.raises(TypeError): + mp_proxy_auxiliary_inst.get_proxy_con(["later_aux"]) + + +def test_get_proxy_con_invalid_aux(mocker, caplog, mp_proxy_auxiliary_inst): mock_get_alias = mocker.patch.object( ConfigRegistry, "get_auxes_alias", return_value="later_aux" ) @@ -226,6 +271,52 @@ def test_get_proxy_con_invalid_(mocker, caplog, mp_proxy_auxiliary_inst): mock_get_alias.assert_called() +def test_getattr_physical_cchannel( + mocker, cchannel_inst, mp_proxy_auxiliary_inst, mock_auxiliaries +): + mocker.patch.object(MpProxyAuxiliary, "_check_compatibility") + mocker.patch.object(MpProxyAuxiliary, "_check_channels_compatibility") + + cchannel_inst.some_attribute = object() + + AUX_LIST_NAMES = ["MockAux1", "MockAux2"] + proxy_inst = MpProxyAuxiliary(cchannel_inst, AUX_LIST_NAMES, name="aux") + proxy_inst.lock = mocker.MagicMock() + + mock_aux1 = importlib.import_module("pykiso.auxiliaries.MockAux1") + + assert isinstance(proxy_inst.channel, type(cchannel_inst)) + + assert mock_aux1.channel._physical_channel is cchannel_inst + + # attribute exists in the physical channel + assert mock_aux1.channel.some_attribute is cchannel_inst.some_attribute + proxy_inst.lock.__enter__.assert_called_once() + proxy_inst.lock.__exit__.assert_called_once() + proxy_inst.lock.reset_mock() + + # attribute exists in the proxy channel + assert mock_aux1.channel.cc_send is not cchannel_inst.cc_send + proxy_inst.lock.__enter__.assert_not_called() + proxy_inst.lock.__exit__.assert_not_called() + + # attribute does not exist in physical channel + with pytest.raises( + AttributeError, match="object has no attribute 'does_not_exist'" + ): + mock_aux1.channel.does_not_exist + proxy_inst.lock.__enter__.assert_called_once() + proxy_inst.lock.__exit__.assert_called_once() + + # attribute does not exist in proxy channel (no physical channel attached) + proxy_inst.lock.reset_mock() + mock_aux1.channel._physical_channel = None + with pytest.raises(AttributeError, match="has no attribute 'does_not_exist'"): + mock_aux1.channel.does_not_exist + proxy_inst.lock.__enter__.assert_not_called() + proxy_inst.lock.__exit__.assert_not_called() + + def test_create_auxiliary_instance(mp_proxy_auxiliary_inst, caplog): with caplog.at_level(logging.INFO): @@ -325,6 +416,7 @@ def test_dispatch_command_invalid(mp_proxy_auxiliary_inst, mock_auxiliaries): def test_run_command(mocker, mp_proxy_auxiliary_inst, mock_auxiliaries): mock_dispatch_command = mocker.patch.object(MpProxyAuxiliary, "_dispatch_command") mocker.patch.object(MpProxyAuxiliary, "_check_compatibility") + mocker.patch.object(MpProxyAuxiliary, "_check_channels_compatibility") mocker.patch.object(mp_proxy_auxiliary_inst, "channel") mock_queue_empty = mocker.patch("queue.Queue.empty", return_value=False) mock_queue_get = mocker.patch( diff --git a/tests/test_proxy_auxiliary.py b/tests/test_proxy_auxiliary.py index e2967def..6c067b6e 100644 --- a/tests/test_proxy_auxiliary.py +++ b/tests/test_proxy_auxiliary.py @@ -7,6 +7,7 @@ # SPDX-License-Identifier: EPL-2.0 ########################################################################## +import importlib import logging import queue import sys @@ -78,9 +79,7 @@ def __init__(self, param_1=None, param_2=None, **kwargs): self.param_1 = param_1 self.param_2 = param_2 self.channel = mock_auxiliaries - super().__init__( - name="mp_aux", - ) + super().__init__(name="mp_aux") _create_auxiliary_instance = mocker.stub(name="_create_auxiliary_instance") _create_auxiliary_instance.return_value = True @@ -128,11 +127,17 @@ def test_init_trace_not_activate(mocker): def test_get_proxy_con_valid(mocker, cchannel_inst, mock_aux_interface): - mocker.patch.object(ProxyAuxiliary, "_check_aux_compatibility") - mocker.patch.object(ProxyAuxiliary, "_check_channels_compatibility") + mock_check_aux = mocker.patch.object(ProxyAuxiliary, "_check_aux_compatibility") + mock_check_channels = mocker.patch.object( + ProxyAuxiliary, "_check_channels_compatibility" + ) + mock_bind_channel = mocker.patch.object(CCProxy, "_bind_channel_info") proxy_inst = ProxyAuxiliary(cchannel_inst, [*AUX_LIST_NAMES, mock_aux_interface]) + mock_check_channels.assert_called_once() + assert mock_check_aux.call_count == len([*AUX_LIST_NAMES, mock_aux_interface]) + assert mock_bind_channel.call_count == len([*AUX_LIST_NAMES, mock_aux_interface]) assert len(proxy_inst.proxy_channels) == 3 assert all(isinstance(items, CCProxy) for items in proxy_inst.proxy_channels) @@ -149,17 +154,66 @@ def test_get_proxy_con_invalid(mocker, caplog, cchannel_inst): assert len(proxy_inst.proxy_channels) == 0 +def test_getattr_physical_cchannel( + mocker, cchannel_inst, mock_aux_interface, mock_auxiliaries +): + mocker.patch.object(ProxyAuxiliary, "_check_aux_compatibility") + mocker.patch.object(ProxyAuxiliary, "_check_channels_compatibility") + + cchannel_inst.some_attribute = object() + + proxy_inst = ProxyAuxiliary(cchannel_inst, [*AUX_LIST_NAMES, mock_aux_interface]) + proxy_inst.lock = mocker.MagicMock() + + mock_aux1 = importlib.import_module("pykiso.auxiliaries.MockAux1") + + assert isinstance(mock_aux1.channel, CCProxy) + assert isinstance(proxy_inst.channel, type(cchannel_inst)) + + assert mock_aux1.channel._physical_channel is cchannel_inst + + # attribute exists in the physical channel instance + assert mock_aux1.channel.some_attribute is cchannel_inst.some_attribute + proxy_inst.lock.__enter__.assert_called_once() + proxy_inst.lock.__exit__.assert_called_once() + proxy_inst.lock.reset_mock() + + # attribute exists in the proxy channel instance + assert mock_aux1.channel.cc_send is not cchannel_inst.cc_send + proxy_inst.lock.__enter__.assert_not_called() + proxy_inst.lock.__exit__.assert_not_called() + + # attribute does not exist in physical channel + with pytest.raises(AttributeError, match="has no attribute 'does_not_exist'"): + mock_aux1.channel.does_not_exist + proxy_inst.lock.__enter__.assert_called_once() + proxy_inst.lock.__exit__.assert_called_once() + + # attribute does not exist in proxy channel (no physical channel attached) + proxy_inst.lock.reset_mock() + mock_aux1.channel._physical_channel = None + with pytest.raises(AttributeError, match="has no attribute 'does_not_exist'"): + mock_aux1.channel.does_not_exist + proxy_inst.lock.__enter__.assert_not_called() + proxy_inst.lock.__exit__.assert_not_called() + + def test_get_proxy_con_pre_load(mocker, cchannel_inst): mocker.patch.object(ConfigRegistry, "get_auxes_alias", return_value="later_aux") mocker.patch.object(ProxyAuxiliary, "_check_channels_compatibility") + mocker.patch.object(CCProxy, "_bind_channel_info") class Linker: def __init__(self): self._aux_cache = AuxCache() + class FakeCCProxy: + def _bind_channel_info(self, *args, **kwargs): + pass + class FakeAux: def __init__(self): - self.channel = True + self.channel = FakeCCProxy() self.is_proxy_capable = True class AuxCache: @@ -171,7 +225,7 @@ def get_instance(self, aux_name): proxy_inst = ProxyAuxiliary(cchannel_inst, ["later_aux"]) assert len(proxy_inst.proxy_channels) == 1 - assert isinstance(proxy_inst.proxy_channels[0], bool) + assert isinstance(proxy_inst.proxy_channels[0], FakeCCProxy) def test_check_aux_compatibility_exception(mocker, cchannel_inst): diff --git a/tests/test_simulated_auxiliary.py b/tests/test_simulated_auxiliary.py index b6a7f6f1..bc990eea 100644 --- a/tests/test_simulated_auxiliary.py +++ b/tests/test_simulated_auxiliary.py @@ -236,10 +236,9 @@ def test_virtual_cfg_output(capsys, prepare_config): """ cfg = cli.parse_config(prepare_config) with pytest.raises(SystemExit): - config_registry = ConfigRegistry(cfg) - config_registry.register_aux_con() + ConfigRegistry.register_aux_con(cfg) exit_code = test_execution.execute(cfg) - config_registry.delete_aux_con() + ConfigRegistry.delete_aux_con() sys.exit(exit_code) output = capsys.readouterr()