diff --git a/app/src/inverter.py b/app/src/inverter.py index 7cb4a7a..dc224ee 100644 --- a/app/src/inverter.py +++ b/app/src/inverter.py @@ -1,15 +1,12 @@ import asyncio -import weakref import logging import json if __name__ == "app.src.inverter": - from app.src.iter_registry import IterRegistry from app.src.config import Config from app.src.mqtt import Mqtt from app.src.infos import Infos else: # pragma: no cover - from iter_registry import IterRegistry from config import Config from mqtt import Mqtt from infos import Infos @@ -17,7 +14,7 @@ else: # pragma: no cover logger_mqtt = logging.getLogger('mqtt') -class Inverter(metaclass=IterRegistry): +class Inverter(): '''class Inverter is a baseclass The class has some class method for managing common resources like a @@ -40,8 +37,6 @@ class Inverter(metaclass=IterRegistry): async_create_remote(): Establish a client connection to the TSUN cloud async_publ_mqtt(): Publish data to MQTT broker ''' - _registry = [] - @classmethod def class_init(cls) -> None: logging.debug('Inverter.class_init') @@ -109,6 +104,3 @@ class Inverter(metaclass=IterRegistry): logging.info('Close MQTT Task') loop.run_until_complete(cls.mqtt.close()) cls.mqtt = None - - def __init__(self): - self._registry.append(weakref.ref(self)) diff --git a/app/src/inverter_base.py b/app/src/inverter_base.py index 026b91b..bd7c700 100644 --- a/app/src/inverter_base.py +++ b/app/src/inverter_base.py @@ -1,3 +1,5 @@ +from abc import abstractmethod +import weakref import asyncio import logging import traceback @@ -6,6 +8,7 @@ from aiomqtt import MqttCodeError from asyncio import StreamReader, StreamWriter if __name__ == "app.src.inverter_base": + from app.src.iter_registry import AbstractIterMeta from app.src.inverter import Inverter from app.src.async_stream import StreamPtr from app.src.async_stream import AsyncStreamClient @@ -13,6 +16,7 @@ if __name__ == "app.src.inverter_base": from app.src.config import Config from app.src.infos import Infos else: # pragma: no cover + from iter_registry import AbstractIterMeta from inverter import Inverter from async_stream import StreamPtr from async_stream import AsyncStreamClient @@ -23,11 +27,43 @@ else: # pragma: no cover logger_mqtt = logging.getLogger('mqtt') -class InverterBase(Inverter): +class InverterIfc(metaclass=AbstractIterMeta): + + @abstractmethod + def __init__(self, reader: StreamReader, writer: StreamWriter, + config_id: str, prot_class, + client_mode: bool): + pass # pragma: no cover + + @abstractmethod + def __enter__(self): + pass # pragma: no cover + + @abstractmethod + def __exit__(self, exc_type, exc, tb): + pass # pragma: no cover + + @abstractmethod + def healthy(self) -> bool: + pass # pragma: no cover + + @abstractmethod + async def disc(self, shutdown_started=False) -> None: + pass # pragma: no cover + + @abstractmethod + async def async_create_remote(self) -> None: + pass # pragma: no cover + + +class InverterBase(InverterIfc, Inverter): + _registry = [] + def __init__(self, reader: StreamReader, writer: StreamWriter, config_id: str, prot_class, client_mode: bool = False): - super().__init__() + Inverter.__init__(self) + self._registry.append(weakref.ref(self)) self.addr = writer.get_extra_info('peername') self.config_id = config_id self.prot_class = prot_class diff --git a/app/src/iter_registry.py b/app/src/iter_registry.py index b84e16f..884d849 100644 --- a/app/src/iter_registry.py +++ b/app/src/iter_registry.py @@ -1,3 +1,4 @@ +from abc import ABCMeta class IterRegistry(type): @@ -6,3 +7,12 @@ class IterRegistry(type): obj = ref() if obj is not None: yield obj + + +class AbstractIterMeta(ABCMeta): + def __iter__(cls): + for ref in cls._registry: + obj = ref() + print(f'obj: {obj}') + if obj is not None: + yield obj diff --git a/app/tests/test_inverter_base.py b/app/tests/test_inverter_base.py new file mode 100644 index 0000000..e1f2f33 --- /dev/null +++ b/app/tests/test_inverter_base.py @@ -0,0 +1,153 @@ +# test_with_pytest.py +import pytest +import asyncio +import sys,gc + +from mock import patch +from enum import Enum +from app.src.infos import Infos +from app.src.config import Config +from app.src.gen3.talent import Talent +from app.src.inverter_base import InverterBase +from app.src.singleton import Singleton +from app.src.protocol_ifc import ProtocolIfcImpl +from app.src.async_stream import AsyncStream, AsyncIfcImpl + +from app.tests.test_modbus_tcp import patch_mqtt_err, patch_mqtt_except, test_port, test_hostname + +pytest_plugins = ('pytest_asyncio',) + +# initialize the proxy statistics +Infos.static_init() + +@pytest.fixture +def config_conn(): + Config.act_config = { + 'mqtt':{ + 'host': test_hostname, + 'port': test_port, + 'user': '', + 'passwd': '' + }, + 'ha':{ + 'auto_conf_prefix': 'homeassistant', + 'discovery_prefix': 'homeassistant', + 'entity_prefix': 'tsun', + 'proxy_node_id': 'test_1', + 'proxy_unique_id': '' + }, + 'tsun':{'enabled': True, 'host': 'test_cloud.local', 'port': 1234}, 'inverters':{'allow_all':True} + } + +@pytest.fixture(scope="module", autouse=True) +def module_init(): + Singleton._instances.clear() + yield + +class FakeReader(): + def __init__(self): + self.on_recv = asyncio.Event() + async def read(self, max_len: int): + await self.on_recv.wait() + return b'' + def feed_eof(self): + return + + +class FakeWriter(): + def write(self, buf: bytes): + return + def get_extra_info(self, sel: str): + if sel == 'peername': + return 'remote.intern' + elif sel == 'sockname': + return 'sock:1234' + assert False + def is_closing(self): + return False + def close(self): + return + async def wait_closed(self): + return + +class TestType(Enum): + RD_TEST_0_BYTES = 1 + RD_TEST_TIMEOUT = 2 + RD_TEST_EXCEPT = 3 + + +test = TestType.RD_TEST_0_BYTES + +@pytest.fixture +def patch_open_connection(): + async def new_conn(conn): + await asyncio.sleep(0) + return FakeReader(), FakeWriter() + + def new_open(host: str, port: int): + global test + if test == TestType.RD_TEST_TIMEOUT: + raise ConnectionRefusedError + elif test == TestType.RD_TEST_EXCEPT: + raise ValueError("Value cannot be negative") # Compliant + return new_conn(None) + + with patch.object(asyncio, 'open_connection', new_open) as conn: + yield conn + + +@pytest.fixture +def patch_healthy(): + with patch.object(AsyncStream, 'healthy') as conn: + yield conn + +def test_protocol_iter(): + ProtocolIfcImpl._registry.clear() + cnt = 0 + ifc = AsyncIfcImpl() + prot = ProtocolIfcImpl(('test.intern', 123), ifc, True) + for p in ProtocolIfcImpl: + assert p == prot + cnt += 1 + del p + del prot + assert cnt == 1 + for p in ProtocolIfcImpl: + assert False + +def test_inverter_iter(): + InverterBase._registry.clear() + cnt = 0 + reader = FakeReader() + writer = FakeWriter() + + with InverterBase(reader, writer, 'tsun', Talent) as inverter: + for inv in InverterBase: + assert inv == inverter + cnt += 1 + del inv + del inverter + assert cnt == 1 + + for inv in InverterBase: + assert False + +def test_method_calls(patch_healthy): + spy = patch_healthy + InverterBase._registry.clear() + reader = FakeReader() + writer = FakeWriter() + + with InverterBase(reader, writer, 'tsun', Talent) as inverter: + assert inverter.local.stream + assert inverter.local.ifc + # inverter.healthy() + for inv in InverterBase: + inv.healthy() + del inv + spy.assert_called_once() + del inverter + cnt = 0 + for inv in InverterBase: + cnt += 1 + assert cnt == 0 diff --git a/app/tests/test_inverter_g3.py b/app/tests/test_inverter_g3.py index 2fbe4e8..702186a 100644 --- a/app/tests/test_inverter_g3.py +++ b/app/tests/test_inverter_g3.py @@ -8,6 +8,7 @@ from enum import Enum from app.src.infos import Infos from app.src.config import Config from app.src.inverter import Inverter +from app.src.inverter_base import InverterBase from app.src.singleton import Singleton from app.src.gen3.inverter_g3 import InverterG3 from app.src.async_stream import AsyncStream @@ -103,18 +104,18 @@ def test_method_calls(patch_healthy): spy = patch_healthy reader = FakeReader() writer = FakeWriter() - Inverter._registry.clear() - + InverterBase._registry.clear() + with InverterG3(reader, writer) as inverter: assert inverter.local.stream assert inverter.local.ifc - for inv in Inverter: + for inv in InverterBase: inv.healthy() del inv spy.assert_called_once() del inverter cnt = 0 - for inv in Inverter: + for inv in InverterBase: cnt += 1 assert cnt == 0 @@ -131,7 +132,7 @@ async def test_remote_conn(config_conn, patch_open_connection): del inverter cnt = 0 - for inv in Inverter: + for inv in InverterBase: print(f'Inverter refs:{gc.get_referrers(inv)}') cnt += 1 assert cnt == 0 @@ -157,8 +158,8 @@ async def test_remote_except(config_conn, patch_open_connection): del inverter cnt = 0 - for inv in Inverter: - print(f'Inverter refs:{gc.get_referrers(inv)}') + for inv in InverterBase: + print(f'InverterBase refs:{gc.get_referrers(inv)}') cnt += 1 assert cnt == 0 diff --git a/app/tests/test_inverter_g3p.py b/app/tests/test_inverter_g3p.py index 603d356..f25b768 100644 --- a/app/tests/test_inverter_g3p.py +++ b/app/tests/test_inverter_g3p.py @@ -7,6 +7,7 @@ from enum import Enum from app.src.infos import Infos from app.src.config import Config from app.src.inverter import Inverter +from app.src.inverter_base import InverterBase from app.src.singleton import Singleton from app.src.gen3plus.inverter_g3p import InverterG3P @@ -96,7 +97,7 @@ def patch_open_connection(): def test_method_calls(): reader = FakeReader() writer = FakeWriter() - Inverter._registry.clear() + InverterBase._registry.clear() with InverterG3P(reader, writer, client_mode=False) as inverter: assert inverter.local.stream