Files
tsun-gen3-proxy/app/tests/test_inverter_base.py
Stefan Allius c956c13d13 Dev 0.11 (#200)
* 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

* GEN3: Invalid Contact Info Msg (#192)

Fixes #191

* 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

* S allius/issue196 (#198)

* fix healthcheck

- on infrastructure with IPv6 support localhost
  might be resolved to an IPv6 adress. Since the
  proxy only support IPv4 for now, we replace
  localhost by 127.0.0.1, to fix this

* merge from main
2024-10-13 18:12:10 +02:00

305 lines
8.4 KiB
Python

# test_with_pytest.py
import pytest
import asyncio
import 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.async_stream import AsyncStream, AsyncStreamClient
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
@pytest.fixture
def patch_unhealthy():
def new_healthy(self):
return False
with patch.object(AsyncStream, 'healthy', new_healthy) as conn:
yield conn
@pytest.fixture
def patch_unhealthy_remote():
def new_healthy(self):
return False
with patch.object(AsyncStreamClient, 'healthy', new_healthy) as conn:
yield conn
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
# call healthy inside the contexter manager
for inv in InverterBase:
assert inv.healthy()
del inv
spy.assert_called_once()
# outside context manager the health function of AsyncStream is not reachable
cnt = 0
for inv in InverterBase:
assert inv.healthy()
cnt += 1
del inv
assert cnt == 1
spy.assert_called_once() # counter don't increase and keep one!
del inverter
cnt = 0
for inv in InverterBase:
print(f'InverterBase refs:{gc.get_referrers(inv)}')
cnt += 1
assert cnt == 0
def test_unhealthy(patch_unhealthy):
_ = patch_unhealthy
InverterBase._registry.clear()
reader = FakeReader()
writer = FakeWriter()
with InverterBase(reader, writer, 'tsun', Talent) as inverter:
assert inverter.local.stream
assert inverter.local.ifc
# call healthy inside the contexter manager
assert not inverter.healthy()
# outside context manager the unhealth AsyncStream is released
cnt = 0
for inv in InverterBase:
assert inv.healthy() # inverter is healthy again (without the unhealty AsyncStream)
cnt += 1
del inv
assert cnt == 1
del inverter
cnt = 0
for inv in InverterBase:
print(f'InverterBase refs:{gc.get_referrers(inv)}')
cnt += 1
assert cnt == 0
def test_unhealthy_remote(patch_unhealthy_remote):
_ = patch_unhealthy
InverterBase._registry.clear()
reader = FakeReader()
writer = FakeWriter()
with InverterBase(reader, writer, 'tsun', Talent) as inverter:
assert inverter.local.stream
assert inverter.local.ifc
# call healthy inside the contexter manager
assert not inverter.healthy()
# outside context manager the unhealth AsyncStream is released
cnt = 0
for inv in InverterBase:
assert inv.healthy() # inverter is healthy again (without the unhealty AsyncStream)
cnt += 1
del inv
assert cnt == 1
del inverter
cnt = 0
for inv in InverterBase:
print(f'InverterBase refs:{gc.get_referrers(inv)}')
cnt += 1
assert cnt == 0
@pytest.mark.asyncio
async def test_remote_conn(config_conn, patch_open_connection):
_ = config_conn
_ = patch_open_connection
assert asyncio.get_running_loop()
reader = FakeReader()
writer = FakeWriter()
with InverterBase(reader, writer, 'tsun', Talent) as inverter:
await inverter.create_remote()
await asyncio.sleep(0)
assert inverter.remote.stream
assert inverter.remote.ifc
# call healthy inside the contexter manager
assert inverter.healthy()
# call healthy outside the contexter manager (__exit__() was called)
assert inverter.healthy()
del inverter
cnt = 0
for inv in InverterBase:
print(f'InverterBase refs:{gc.get_referrers(inv)}')
cnt += 1
assert cnt == 0
@pytest.mark.asyncio
async def test_unhealthy_remote(config_conn, patch_open_connection, patch_unhealthy_remote):
_ = config_conn
_ = patch_open_connection
_ = patch_unhealthy_remote
assert asyncio.get_running_loop()
InverterBase._registry.clear()
reader = FakeReader()
writer = FakeWriter()
with InverterBase(reader, writer, 'tsun', Talent) as inverter:
assert inverter.local.stream
assert inverter.local.ifc
await inverter.create_remote()
await asyncio.sleep(0)
assert inverter.remote.stream
assert inverter.remote.ifc
assert inverter.local.ifc.healthy()
assert not inverter.remote.ifc.healthy()
# call healthy inside the contexter manager
assert not inverter.healthy()
# outside context manager the unhealth AsyncStream is released
cnt = 0
for inv in InverterBase:
assert inv.healthy() # inverter is healthy again (without the unhealty AsyncStream)
cnt += 1
del inv
assert cnt == 1
del inverter
cnt = 0
for inv in InverterBase:
print(f'InverterBase refs:{gc.get_referrers(inv)}')
cnt += 1
assert cnt == 0
@pytest.mark.asyncio
async def test_remote_disc(config_conn, patch_open_connection):
_ = config_conn
_ = patch_open_connection
assert asyncio.get_running_loop()
reader = FakeReader()
writer = FakeWriter()
with InverterBase(reader, writer, 'tsun', Talent) as inverter:
await inverter.create_remote()
await asyncio.sleep(0)
assert inverter.remote.stream
# call disc inside the contexter manager
await inverter.disc()
# call disc outside the contexter manager (__exit__() was called)
await inverter.disc()
del inverter
cnt = 0
for inv in InverterBase:
print(f'InverterBase refs:{gc.get_referrers(inv)}')
cnt += 1
assert cnt == 0