Refactoring async stream (#194)

* GEN3: Invalid Contact Info Msg
Fixes #191

* introduce ifc with FIFOs

* add object factory

* use AsyncIfc class with FIFO

* declare more methods as classmethods

* - refactoring

- remove _forward_buffer
- make async_write private

* remove _forward_buffer

* refactoring

* avoid mqtt handling for invalid serial numbers

* add two more callbacks

* FIX update_header_cb handling

* split AsyncStream in two classes

* split ConnectionG3(P) in server and client class

* update class diagramm

* refactor server creation

* remove duplicated imports

* reduce code duplication

* move StremPtr instances into Inverter class

* resolution of connection classes

- remove ConnectionG3Client
- remove ConnectionG3Server
- remove ConnectionG3PClient
- remove ConnectionG3PServer

* fix server connections

* fix client loop closing

* don't overwrite self.remote in constructor

* update class diagramm

* fixes

- fixes null pointer accesses
- initalize AsyncStreamClient with proper
  StreamPtr instance

* add close callback

* refactor close handling

* remove connection classes

* move more code into InverterBase class

* remove test_inverter_base.py

* add abstract inverter interface class

* initial commit

* fix sonar qube warnings

* rename class Inverter into Proxy

* fix typo

* move class InverterIfc into a separate file

* add more testcases

* use ProtocolIfc class

* add unit tests for AsyncStream class

* icrease test coverage

* reduce cognitive complexity

* increase test coverage

* increase tes coverage

* simplify heartbeat handler

* remove obsolete tx_get method

* add more unittests

* update changelog

* remove __del__ method for proper gc runs

* check releasing of ModbusConn instances

* call garbage collector to release unreachable objs

* decrease ref counter after the with block
This commit is contained in:
Stefan Allius
2024-10-13 16:07:01 +02:00
committed by GitHub
parent 166a856705
commit 62ea2a9e6f
38 changed files with 3245 additions and 1882 deletions

View File

@@ -9,12 +9,9 @@ from app.src.singleton import Singleton
from app.src.config import Config
from app.src.infos import Infos
from app.src.mqtt import Mqtt
from app.src.inverter_base import InverterBase
from app.src.messages import Message, State
from app.src.inverter import Inverter
from app.src.modbus_tcp import ModbusConn, ModbusTcp
from app.src.mqtt import Mqtt
from app.src.messages import Message, State
from app.src.inverter import Inverter
from app.src.proxy import Proxy
from app.src.modbus_tcp import ModbusConn, ModbusTcp
@@ -75,55 +72,93 @@ def config_conn(test_hostname, test_port):
}
class TestType(Enum):
class FakeReader():
RD_TEST_0_BYTES = 1
RD_TEST_TIMEOUT = 2
RD_TEST_13_BYTES = 3
RD_TEST_SW_EXCEPT = 4
RD_TEST_OS_ERROR = 5
test = TestType.RD_TEST_0_BYTES
class FakeReader():
def __init__(self):
self.on_recv = asyncio.Event()
self.test = self.RD_TEST_0_BYTES
async def read(self, max_len: int):
print(f'fakeReader test: {self.test}')
await self.on_recv.wait()
if test == TestType.RD_TEST_0_BYTES:
if self.test == self.RD_TEST_0_BYTES:
return b''
elif test == TestType.RD_TEST_TIMEOUT:
elif self.test == self.RD_TEST_13_BYTES:
print('fakeReader return 13 bytes')
self.test = self.RD_TEST_0_BYTES
return b'test-data-req'
elif self.test == self.RD_TEST_TIMEOUT:
raise TimeoutError
elif self.test == self.RD_TEST_SW_EXCEPT:
self.test = self.RD_TEST_0_BYTES
self.unknown_var += 1
elif self.test == self.RD_TEST_OS_ERROR:
self.test = self.RD_TEST_0_BYTES
raise ConnectionRefusedError
def feed_eof(self):
return
class FakeWriter():
def __init__(self, conn='remote.intern'):
self.conn = conn
self.closing = False
def write(self, buf: bytes):
return
async def drain(self):
await asyncio.sleep(0)
def get_extra_info(self, sel: str):
if sel == 'peername':
return 'remote.intern'
return self.conn
elif sel == 'sockname':
return 'sock:1234'
assert False
def is_closing(self):
return False
return self.closing
def close(self):
return
self.closing = True
async def wait_closed(self):
return
await asyncio.sleep(0)
@pytest.fixture
def patch_open():
async def new_conn(conn):
await asyncio.sleep(0)
return FakeReader(), FakeWriter()
return FakeReader(), FakeWriter(conn)
def new_open(host: str, port: int):
global test
if test == TestType.RD_TEST_TIMEOUT:
raise TimeoutError
return new_conn(None)
return new_conn(f'{host}:{port}')
with patch.object(asyncio, 'open_connection', new_open) as conn:
yield conn
@pytest.fixture
def patch_open_timeout():
def new_open(host: str, port: int):
raise TimeoutError
with patch.object(asyncio, 'open_connection', new_open) as conn:
yield conn
@pytest.fixture
def patch_open_value_error():
def new_open(host: str, port: int):
raise ValueError
with patch.object(asyncio, 'open_connection', new_open) as conn:
yield conn
@pytest.fixture
def patch_open_conn_abort():
def new_open(host: str, port: int):
raise ConnectionAbortedError
with patch.object(asyncio, 'open_connection', new_open) as conn:
yield conn
@@ -154,13 +189,18 @@ async def test_modbus_conn(patch_open):
_ = patch_open
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
async with ModbusConn('test.local', 1234) as stream:
async with ModbusConn('test.local', 1234) as inverter:
stream = inverter.local.stream
assert stream.node_id == 'G3P'
assert stream.addr == ('test.local', 1234)
assert type(stream.reader) is FakeReader
assert type(stream.writer) is FakeWriter
assert stream.addr == ('test.local:1234')
assert type(stream.ifc._reader) is FakeReader
assert type(stream.ifc._writer) is FakeWriter
assert Infos.stat['proxy']['Inverter_Cnt'] == 1
del inverter
for _ in InverterBase:
assert False
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
@pytest.mark.asyncio
@@ -171,13 +211,47 @@ async def test_modbus_no_cnf():
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
@pytest.mark.asyncio
async def test_modbus_cnf1(config_conn, patch_open):
async def test_modbus_timeout(config_conn, patch_open_timeout):
_ = config_conn
_ = patch_open
global test
_ = patch_open_timeout
assert asyncio.get_running_loop()
Inverter.class_init()
test = TestType.RD_TEST_TIMEOUT
Proxy.class_init()
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
loop = asyncio.get_event_loop()
ModbusTcp(loop)
await asyncio.sleep(0.01)
for m in Message:
if (m.node_id == 'inv_2'):
assert False
await asyncio.sleep(0.01)
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
@pytest.mark.asyncio
async def test_modbus_value_err(config_conn, patch_open_value_error):
_ = config_conn
_ = patch_open_value_error
assert asyncio.get_running_loop()
Proxy.class_init()
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
loop = asyncio.get_event_loop()
ModbusTcp(loop)
await asyncio.sleep(0.01)
for m in Message:
if (m.node_id == 'inv_2'):
assert False
await asyncio.sleep(0.01)
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
@pytest.mark.asyncio
async def test_modbus_conn_abort(config_conn, patch_open_conn_abort):
_ = config_conn
_ = patch_open_conn_abort
assert asyncio.get_running_loop()
Proxy.class_init()
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
loop = asyncio.get_event_loop()
@@ -195,10 +269,8 @@ async def test_modbus_cnf2(config_conn, patch_no_mqtt, patch_open):
_ = config_conn
_ = patch_open
_ = patch_no_mqtt
global test
assert asyncio.get_running_loop()
Inverter.class_init()
test = TestType.RD_TEST_0_BYTES
Proxy.class_init()
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
ModbusTcp(asyncio.get_event_loop())
@@ -209,7 +281,7 @@ async def test_modbus_cnf2(config_conn, patch_no_mqtt, patch_open):
test += 1
assert Infos.stat['proxy']['Inverter_Cnt'] == 1
m.shutdown_started = True
m.reader.on_recv.set()
m.ifc._reader.on_recv.set()
del m
assert 1 == test
@@ -221,10 +293,8 @@ async def test_modbus_cnf3(config_conn, patch_no_mqtt, patch_open):
_ = config_conn
_ = patch_open
_ = patch_no_mqtt
global test
assert asyncio.get_running_loop()
Inverter.class_init()
test = TestType.RD_TEST_0_BYTES
Proxy.class_init()
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
ModbusTcp(asyncio.get_event_loop(), tim_restart= 0)
@@ -236,13 +306,13 @@ async def test_modbus_cnf3(config_conn, patch_no_mqtt, patch_open):
test += 1
if test == 1:
m.shutdown_started = False
m.reader.on_recv.set()
m.ifc._reader.on_recv.set()
await asyncio.sleep(0.1)
assert m.state == State.closed
await asyncio.sleep(0.1)
else:
m.shutdown_started = True
m.reader.on_recv.set()
m.ifc._reader.on_recv.set()
del m
assert 2 == test
@@ -254,10 +324,8 @@ async def test_mqtt_err(config_conn, patch_mqtt_err, patch_open):
_ = config_conn
_ = patch_open
_ = patch_mqtt_err
global test
assert asyncio.get_running_loop()
Inverter.class_init()
test = TestType.RD_TEST_0_BYTES
Proxy.class_init()
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
ModbusTcp(asyncio.get_event_loop(), tim_restart= 0)
@@ -269,13 +337,14 @@ async def test_mqtt_err(config_conn, patch_mqtt_err, patch_open):
test += 1
if test == 1:
m.shutdown_started = False
m.reader.on_recv.set()
m.ifc._reader.on_recv.set()
await asyncio.sleep(0.1)
assert m.state == State.closed
await asyncio.sleep(0.1)
await asyncio.sleep(0.1)
else:
m.shutdown_started = True
m.reader.on_recv.set()
m.ifc._reader.on_recv.set()
del m
await asyncio.sleep(0.01)
@@ -286,10 +355,8 @@ async def test_mqtt_except(config_conn, patch_mqtt_except, patch_open):
_ = config_conn
_ = patch_open
_ = patch_mqtt_except
global test
assert asyncio.get_running_loop()
Inverter.class_init()
test = TestType.RD_TEST_0_BYTES
Proxy.class_init()
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
ModbusTcp(asyncio.get_event_loop(), tim_restart= 0)
@@ -301,13 +368,13 @@ async def test_mqtt_except(config_conn, patch_mqtt_except, patch_open):
test += 1
if test == 1:
m.shutdown_started = False
m.reader.on_recv.set()
m.ifc._reader.on_recv.set()
await asyncio.sleep(0.1)
assert m.state == State.closed
await asyncio.sleep(0.1)
else:
m.shutdown_started = True
m.reader.on_recv.set()
m.ifc._reader.on_recv.set()
del m
await asyncio.sleep(0.01)