Files
tsun-gen3-proxy/app/tests/test_modbus_tcp.py
Stefan Allius bfea38d9da Dev 0.11 (#190)
* Code Cleanup (#158)


* print coverage report

* create sonar-project property file

* install all py dependencies in one step

* code cleanup

* reduce cognitive complexity

* do not build on *.yml changes

* optimise versionstring handling (#159)

- Reading the version string from the image updates
  it even if the image is re-pulled without re-deployment

* fix linter warning

* exclude *.pyi filese

* ignore some rules for tests

* cleanup (#160)

* Sonar qube 3 (#163)

fix SonarQube warnings in modbus.py

* Sonar qube 3 (#164)


* fix SonarQube warnings

* Sonar qube 3 (#165)

* cleanup

* Add support for TSUN Titan inverter
Fixes #161


* fix SonarQube warnings

* fix error

* rename field "config"

* SonarQube reads flake8 output

* don't stop on flake8 errors

* flake8 scan only app/src for SonarQube

* update flake8 run

* ignore flake8 C901

* cleanup

* fix linter warnings

* ignore changed *.yml files

* read sensor list solarman data packets

* catch 'No route to' error and log only in debug mode

* fix unit tests

* add sensor_list configuration

* adapt unit tests

* fix SonarQube warnings

* Sonar qube 3 (#166)

* add unittests for mqtt.py

* add mock

* move test requirements into a file

* fix unit tests

* fix formating

* initial version

* fix SonarQube warning

* Sonar qube 4 (#169)

* add unit test for inverter.py

* fix SonarQube warning

* Sonar qube 5 (#170)

* fix SonarLints warnings

* use random IP adresses for unit tests

* Docker: The description ist missing (#171)

Fixes #167

* S allius/issue167 (#172)

* cleanup

* Sonar qube 6 (#174)

* test class ModbusConn

* Sonar qube 3 (#178)

* add more unit tests

* GEN3: don't crash on overwritten msg in the receive buffer

* improve test coverage und reduce test delays

* reduce cognitive complexity

* fix merge

* fix merge conflikt

* fix merge conflict

* S allius/issue182 (#183)

* GEN3: After inverter firmware update the 'Unknown Msg Type' increases continuously
Fixes #182

* add support for Controller serial no and MAC

* test hardening

* GEN3: add support for new messages of version 3 firmwares

* bump libraries to latest versions

- bump aiomqtt to version 2.3.0
- bump aiohttp to version 3.10.5

* improve test coverage

* reduce cognective complexity

* fix target preview

* remove dubbled fixtures

* increase test coverage

* Update README.md (#185)

update badges

* S allius/issue186 (#187)

* Parse more values in Server Mode
Fixes #186

* read OUTPUT_COEFFICIENT and MAC_ADDR in SrvMode

* fix unit test

* increase test coverage

* S allius/issue186 (#188)

* increase test coverage

* update changelog

* add dokumentation

* change default config

* Update README.md (#189)

Config file is now foldable
2024-09-16 00:45:36 +02:00

315 lines
9.0 KiB
Python

# test_with_pytest.py
import pytest
import asyncio
from aiomqtt import MqttCodeError
from mock import patch
from enum import Enum
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.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.modbus_tcp import ModbusConn, ModbusTcp
pytest_plugins = ('pytest_asyncio',)
# initialize the proxy statistics
Infos.static_init()
@pytest.fixture(scope="module", autouse=True)
def module_init():
Singleton._instances.clear()
yield
@pytest.fixture(scope="module")
def test_port():
return 1883
@pytest.fixture(scope="module")
def test_hostname():
# if getenv("GITHUB_ACTIONS") == "true":
# return 'mqtt'
# else:
return 'test.mosquitto.org'
@pytest.fixture
def config_conn(test_hostname, test_port):
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': ''
},
'inverters':{
'allow_all': True,
"R170000000000001":{
'node_id': 'inv_1'
},
"Y170000000000001":{
'node_id': 'inv_2',
'monitor_sn': 2000000000,
'modbus_polling': True,
'suggested_area': "",
'sensor_list': 0x2b0,
'client_mode':{
'host': '192.168.0.1',
'port': 8899
}
}
}
}
class TestType(Enum):
RD_TEST_0_BYTES = 1
RD_TEST_TIMEOUT = 2
test = TestType.RD_TEST_0_BYTES
class FakeReader():
def __init__(self):
self.on_recv = asyncio.Event()
async def read(self, max_len: int):
await self.on_recv.wait()
if test == TestType.RD_TEST_0_BYTES:
return b''
elif test == TestType.RD_TEST_TIMEOUT:
raise TimeoutError
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
@pytest.fixture
def patch_open():
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 TimeoutError
return new_conn(None)
with patch.object(asyncio, 'open_connection', new_open) as conn:
yield conn
@pytest.fixture
def patch_no_mqtt():
with patch.object(Mqtt, 'publish') as conn:
yield conn
@pytest.fixture
def patch_mqtt_err():
def new_publish(self, key, data):
raise MqttCodeError(None)
with patch.object(Mqtt, 'publish', new_publish) as conn:
yield conn
@pytest.fixture
def patch_mqtt_except():
def new_publish(self, key, data):
raise ValueError("Test")
with patch.object(Mqtt, 'publish', new_publish) as conn:
yield conn
@pytest.mark.asyncio
async def test_modbus_conn(patch_open):
_ = patch_open
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
async with ModbusConn('test.local', 1234) as 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 Infos.stat['proxy']['Inverter_Cnt'] == 1
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
@pytest.mark.asyncio
async def test_modbus_no_cnf():
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
loop = asyncio.get_event_loop()
ModbusTcp(loop)
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
@pytest.mark.asyncio
async def test_modbus_cnf1(config_conn, patch_open):
_ = config_conn
_ = patch_open
global test
assert asyncio.get_running_loop()
Inverter.class_init()
test = TestType.RD_TEST_TIMEOUT
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_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
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
ModbusTcp(asyncio.get_event_loop())
await asyncio.sleep(0.01)
test = 0
for m in Message:
if (m.node_id == 'inv_2'):
test += 1
assert Infos.stat['proxy']['Inverter_Cnt'] == 1
m.shutdown_started = True
m.reader.on_recv.set()
del m
assert 1 == test
await asyncio.sleep(0.01)
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
@pytest.mark.asyncio
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
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
ModbusTcp(asyncio.get_event_loop(), tim_restart= 0)
await asyncio.sleep(0.01)
test = 0
for m in Message:
if (m.node_id == 'inv_2'):
assert Infos.stat['proxy']['Inverter_Cnt'] == 1
test += 1
if test == 1:
m.shutdown_started = False
m.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()
del m
assert 2 == test
await asyncio.sleep(0.01)
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
@pytest.mark.asyncio
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
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
ModbusTcp(asyncio.get_event_loop(), tim_restart= 0)
await asyncio.sleep(0.01)
test = 0
for m in Message:
if (m.node_id == 'inv_2'):
assert Infos.stat['proxy']['Inverter_Cnt'] == 1
test += 1
if test == 1:
m.shutdown_started = False
m.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()
del m
await asyncio.sleep(0.01)
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
@pytest.mark.asyncio
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
assert Infos.stat['proxy']['Inverter_Cnt'] == 0
ModbusTcp(asyncio.get_event_loop(), tim_restart= 0)
await asyncio.sleep(0.01)
test = 0
for m in Message:
if (m.node_id == 'inv_2'):
assert Infos.stat['proxy']['Inverter_Cnt'] == 1
test += 1
if test == 1:
m.shutdown_started = False
m.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()
del m
await asyncio.sleep(0.01)
assert Infos.stat['proxy']['Inverter_Cnt'] == 0