Skip to content

Commit f7fff09

Browse files
authored
transport 100% coverage. (#1941)
1 parent 23f1f38 commit f7fff09

File tree

9 files changed

+70
-75
lines changed

9 files changed

+70
-75
lines changed

pymodbus/transport/serialtransport.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def __init__(self, loop, protocol, *args, **kwargs) -> None:
3131
def setup(self):
3232
"""Prepare to read/write."""
3333
if os.name == "nt" or self.force_poll:
34-
self.poll_task = asyncio.create_task(self._polling_task())
34+
self.poll_task = asyncio.create_task(self.polling_task())
3535
self.poll_task.set_name("SerialTransport poll")
3636
else:
3737
self.async_loop.add_reader(self.sync_serial.fileno(), self._read_ready)
@@ -41,9 +41,6 @@ def close(self, exc: Exception | None = None) -> None:
4141
"""Close the transport gracefully."""
4242
if not self.sync_serial:
4343
return
44-
with contextlib.suppress(Exception):
45-
self.sync_serial.flush()
46-
4744
self.flush()
4845
if self.poll_task:
4946
self.poll_task.cancel()
@@ -61,7 +58,7 @@ def write(self, data) -> None:
6158
"""Write some data to the transport."""
6259
self._write_buffer.append(data)
6360
if not self.poll_task:
64-
self.async_loop.add_writer(self.sync_serial.fileno(), self._write_ready)
61+
self.async_loop.add_writer(self.sync_serial.fileno(), self.write_ready)
6562

6663
def flush(self) -> None:
6764
"""Clear output buffer and stops any more data being written."""
@@ -131,15 +128,15 @@ def _read_ready(self):
131128
except serial.SerialException as exc:
132129
self.close(exc=exc)
133130

134-
def _write_ready(self):
131+
def write_ready(self):
135132
"""Asynchronously write buffered data."""
136133
data = b"".join(self._write_buffer)
137134
try:
138135
if (nlen := self.sync_serial.write(data)) < len(data):
139136
self._write_buffer = [data[nlen:]]
140137
if not self.poll_task:
141138
self.async_loop.add_writer(
142-
self.sync_serial.fileno(), self._write_ready
139+
self.sync_serial.fileno(), self.write_ready
143140
)
144141
return
145142
self.flush()
@@ -148,19 +145,17 @@ def _write_ready(self):
148145
except serial.SerialException as exc:
149146
self.close(exc=exc)
150147

151-
async def _polling_task(self):
148+
async def polling_task(self):
152149
"""Poll and try to read/write."""
153150
try:
154-
while True:
151+
while self.sync_serial:
155152
await asyncio.sleep(self._poll_wait_time)
156153
while self._write_buffer:
157-
self._write_ready()
154+
self.write_ready()
158155
if self.sync_serial.in_waiting:
159156
self._read_ready()
160-
except serial.SerialException as exc:
161-
self.close(exc=exc)
162157
except asyncio.CancelledError:
163-
pass
158+
self.close("Cancelled")
164159

165160

166161
async def create_serial_connection(

pymodbus/transport/transport.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
import asyncio
5252
import dataclasses
5353
import ssl
54-
import sys
5554
from contextlib import suppress
5655
from enum import Enum
5756
from typing import Any, Callable, Coroutine
@@ -62,14 +61,6 @@
6261

6362
NULLMODEM_HOST = "__pymodbus_nullmodem"
6463

65-
if sys.version_info.minor == 11:
66-
USEEXCEPTIONS: tuple[type[Any], type[Any]] | type[Any] = OSError
67-
else:
68-
USEEXCEPTIONS = (
69-
asyncio.TimeoutError,
70-
OSError,
71-
)
72-
7364

7465
class CommType(Enum):
7566
"""Type of transport."""
@@ -254,13 +245,9 @@ async def transport_connect(self) -> bool:
254245
self.call_create(),
255246
timeout=self.comm_params.timeout_connect,
256247
)
257-
except USEEXCEPTIONS as exc:
248+
except (asyncio.TimeoutError, OSError) as exc: # pylint: disable=overlapping-except
258249
Log.warning("Failed to connect {}", exc)
259-
# self.transport_close(intern=True, reconnect=True)
260250
return False
261-
except Exception as exc:
262-
Log.warning("Failed to connect UNKNOWN EXCEPTION {}", exc)
263-
raise
264251
return bool(self.transport)
265252

266253
async def transport_listen(self) -> bool:

pyproject.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ documentation = [
6868
development = [
6969
"build>=1.0.3",
7070
"codespell>=2.2.2",
71-
"coverage>=7.1.0",
71+
"coverage>=7.4.0",
7272
"mypy>=1.6.0",
7373
"pylint>=2.17.2",
7474
"pytest>=7.3.1",
@@ -232,6 +232,14 @@ include = [
232232
]
233233
omit = ["examples/contrib/"]
234234

235+
[tool.coverage.report]
236+
exclude_lines = [
237+
"_check_system_health",
238+
"if __name__ == .__main__.:",
239+
]
240+
241+
ignore_errors = true
242+
235243
[tool.codespell]
236244
skip = "./build,./doc/source/_static,venv,.venv,.git,htmlcov,CHANGELOG.rst,.mypy_cache"
237245
ignore-words-list = "asend"

test/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ async def _check_system_health():
5151
start_tasks = {task.get_name(): task for task in asyncio.all_tasks()}
5252
yield
5353
await asyncio.sleep(0.1)
54-
all_clean = True
5554
for count in range(10):
55+
all_clean = True
5656
error_text = f"ERROR tasks/threads hanging after {count} retries:\n"
5757
for thread in thread_enumerate():
5858
name = thread.getName()
@@ -72,7 +72,7 @@ async def _check_system_health():
7272
all_clean = False
7373
if all_clean:
7474
break
75-
await asyncio.sleep(1)
75+
await asyncio.sleep(0.3)
7676
assert all_clean, error_text
7777
assert not NullModem.is_dirty()
7878

test/sub_examples/test_client_server_sync.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@
2525
from pymodbus.server import ServerStop
2626

2727

28-
if os.name == "nt":
29-
SLEEPING = 5
30-
else:
31-
SLEEPING = 1
28+
SLEEPING = 5 if os.name == "nt" else 1
3229

3330

3431
@pytest.mark.parametrize("use_host", ["localhost"])

test/sub_transport/conftest.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
CommType,
1212
ModbusProtocol,
1313
)
14-
from pymodbus.transport.transport import NullModem
1514

1615

1716
class DummyProtocol(ModbusProtocol):
@@ -50,12 +49,6 @@ def prepare_dummy_protocol():
5049
return DummyProtocol
5150

5251

53-
@pytest.fixture(name="cwd_certificate")
54-
def prepare_cwd_certificate():
55-
"""Prepare path to certificate."""
56-
return os.path.dirname(__file__) + "/../../examples/certificates/pymodbus."
57-
58-
5952
@pytest.fixture(name="use_comm_type")
6053
def prepare_dummy_use_comm_type():
6154
"""Return default comm_type."""
@@ -138,15 +131,3 @@ def prepare_transport_server(use_cls):
138131
True, certfile=cwd + "crt", keyfile=cwd + "key"
139132
)
140133
return transport
141-
142-
143-
@pytest.fixture(name="nullmodem")
144-
def prepare_nullmodem():
145-
"""Prepare nullmodem object."""
146-
return NullModem(mock.Mock())
147-
148-
149-
@pytest.fixture(name="nullmodem_server")
150-
def prepare_nullmodem_server():
151-
"""Prepare nullmodem object."""
152-
return NullModem(mock.Mock())

test/sub_transport/test_basic.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Test transport."""
22
import asyncio
3+
import os
34
from unittest import mock
45

56
import pytest
7+
import serial
68

79
from pymodbus.transport import (
810
CommType,
@@ -22,7 +24,7 @@
2224
]
2325

2426

25-
class TestBasicModbusProtocol:
27+
class TestBasicModbusProtocol: # pylint: disable=too-many-public-methods
2628
"""Test transport module."""
2729

2830
@staticmethod
@@ -53,6 +55,11 @@ async def test_init_serial(self, client, server):
5355
server.comm_params.sslctx = None
5456
assert server.is_server
5557

58+
async def test_init_source_addr(self, use_clc):
59+
"""Test callbacks."""
60+
_client = ModbusProtocol(use_clc, True)
61+
62+
5663
async def test_connect(self, client, dummy_protocol):
5764
"""Test properties."""
5865
client.loop = None
@@ -279,13 +286,6 @@ def test_generate_ssl(self, use_clc):
279286
class TestBasicSerial:
280287
"""Test transport serial module."""
281288

282-
@staticmethod
283-
@pytest.fixture(name="use_port")
284-
def get_port_in_class(base_ports):
285-
"""Return next port."""
286-
base_ports[__class__.__name__] += 1
287-
return base_ports[__class__.__name__]
288-
289289
async def test_init(self):
290290
"""Test null modem init."""
291291
SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy")
@@ -330,3 +330,35 @@ async def test_external_methods(self):
330330
assert transport
331331
assert protocol
332332
transport.close()
333+
334+
async def test_serial_polling(self):
335+
"""Test polling."""
336+
if os.name == "nt":
337+
return
338+
339+
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy")
340+
comm.sync_serial = mock.MagicMock()
341+
comm.sync_serial.read.side_effect = asyncio.CancelledError("test")
342+
await comm.polling_task()
343+
344+
async def test_serial_ready(self):
345+
"""Test polling."""
346+
if os.name == "nt":
347+
return
348+
349+
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy")
350+
comm.sync_serial = mock.MagicMock()
351+
comm.sync_serial.read.side_effect = serial.SerialException("test")
352+
await comm.polling_task()
353+
354+
async def test_serial_write_ready(self):
355+
"""Test polling."""
356+
if os.name == "nt":
357+
return
358+
359+
comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy")
360+
comm.sync_serial = mock.MagicMock()
361+
comm.sync_serial.write.side_effect = BlockingIOError("test")
362+
comm.write_ready()
363+
comm.sync_serial.write.side_effect = serial.SerialException("test")
364+
comm.write_ready()

test/sub_transport/test_comm.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"""Test transport."""
22
import asyncio
3-
import sys
43
import time
54
from unittest import mock
65

76
import pytest
87

8+
from pymodbus.logging import Log
99
from pymodbus.transport import (
1010
CommType,
1111
ModbusProtocol,
@@ -37,7 +37,7 @@ def get_port_in_class(base_ports):
3737
)
3838
async def test_connect(self, client, use_port):
3939
"""Test connect()."""
40-
print(f"JAN test_connect --> {use_port}", file=sys.stderr)
40+
Log.debug("test_connect {}", use_port)
4141
start = time.time()
4242
assert not await client.transport_connect()
4343
delta = time.time() - start
@@ -55,7 +55,7 @@ async def test_connect(self, client, use_port):
5555
)
5656
async def test_connect_not_ok(self, client, use_port):
5757
"""Test connect()."""
58-
print(f"JAN test_connect_not_ok --> {use_port}", file=sys.stderr)
58+
Log.debug("test_connect_not_ok {}", use_port)
5959
start = time.time()
6060
assert not await client.transport_connect()
6161
delta = time.time() - start
@@ -73,7 +73,7 @@ async def test_connect_not_ok(self, client, use_port):
7373
)
7474
async def test_listen(self, server, use_port):
7575
"""Test listen()."""
76-
print(f"JAN test_listen --> {use_port}", file=sys.stderr)
76+
Log.debug("test_listen {}", use_port)
7777
assert await server.transport_listen()
7878
assert server.transport
7979
server.transport_close()
@@ -89,7 +89,7 @@ async def test_listen(self, server, use_port):
8989
)
9090
async def test_listen_not_ok(self, server, use_port):
9191
"""Test listen()."""
92-
print(f"JAN test_listen_not_ok --> {use_port}", file=sys.stderr)
92+
Log.debug("test_listen_not_ok {}", use_port)
9393
assert not await server.transport_listen()
9494
assert not server.transport
9595
server.transport_close()
@@ -105,7 +105,7 @@ async def test_listen_not_ok(self, server, use_port):
105105
)
106106
async def test_connected(self, client, server, use_comm_type, use_port):
107107
"""Test connection and data exchange."""
108-
print(f"JAN test_connected --> {use_port}", file=sys.stderr)
108+
Log.debug("test_connected {}", use_port)
109109
assert await server.transport_listen()
110110
assert await client.transport_connect()
111111
await asyncio.sleep(0.5)
@@ -144,7 +144,7 @@ def wrapped_write(self, data):
144144
)
145145
async def test_split_serial_packet(self, client, server, use_port):
146146
"""Test connection and data exchange."""
147-
print(f"JAN test_split_serial_packet --> {use_port}", file=sys.stderr)
147+
Log.debug("test_split_serial_packet {}", use_port)
148148
assert await server.transport_listen()
149149
assert await client.transport_connect()
150150
await asyncio.sleep(0.5)
@@ -173,7 +173,7 @@ async def test_split_serial_packet(self, client, server, use_port):
173173
)
174174
async def test_serial_poll(self, client, server, use_port):
175175
"""Test connection and data exchange."""
176-
print(f"JAN test_serial_poll --> {use_port}", file=sys.stderr)
176+
Log.debug("test_serial_poll {}", use_port)
177177
assert await server.transport_listen()
178178
SerialTransport.force_poll = True
179179
assert await client.transport_connect()
@@ -200,7 +200,7 @@ async def test_serial_poll(self, client, server, use_port):
200200
)
201201
async def test_connected_multiple(self, client, server, use_port):
202202
"""Test connection and data exchange."""
203-
print(f"JAN test_connected_multiple --> {use_port}", file=sys.stderr)
203+
Log.debug("test_connected {}", use_port)
204204
client.comm_params.reconnect_delay = 0.0
205205
assert await server.transport_listen()
206206
assert await client.transport_connect()

test/test_framers.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,6 @@ def fixture_rtu_framer():
2929
"""RTU framer."""
3030
return ModbusRtuFramer(ClientDecoder())
3131

32-
@pytest.fixture(name="socket_framer")
33-
def fixture_socket_framer():
34-
"""Socket framer."""
35-
return ModbusSocketFramer(ClientDecoder())
36-
3732
@pytest.fixture(name="ascii_framer")
3833
def fixture_ascii_framer():
3934
"""Ascii framer."""

0 commit comments

Comments
 (0)