From c31353b079fa88430e8ad434d82be7a8c67a5e0f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 28 Jul 2023 13:00:52 +0200 Subject: [PATCH] transport_emulator, part II. --- examples/client_async.py | 8 +- examples/client_sync.py | 8 +- examples/datastore_simulator.py | 5 +- examples/helper.py | 28 +- examples/simple_async_client.py | 4 +- examples/simple_sync_client.py | 4 +- pymodbus/client/base.py | 2 +- pymodbus/server/async_io.py | 11 +- pymodbus/transport/transport.py | 167 ++++++----- test/conftest.py | 28 +- test/{ => sub_client}/test_client.py | 0 .../test_client_faulty_response.py | 0 test/{ => sub_client}/test_client_sync.py | 0 test/sub_examples/conftest.py | 19 +- test/sub_examples/test_client_server_async.py | 55 ++-- test/sub_examples/test_client_server_sync.py | 67 ++--- test/sub_examples/test_examples.py | 274 ++++++++---------- test/{ => sub_server}/test_server_asyncio.py | 0 test/{ => sub_server}/test_server_context.py | 0 .../{ => sub_server}/test_server_multidrop.py | 0 test/sub_transport/conftest.py | 10 +- test/sub_transport/test_basic.py | 23 +- test/sub_transport/test_comm.py | 50 ++-- test/sub_transport/test_nullmodem.py | 179 ++++++------ test/sub_transport/test_protocol.py | 8 +- test/sub_transport/test_reconnect.py | 46 ++- 26 files changed, 476 insertions(+), 520 deletions(-) rename test/{ => sub_client}/test_client.py (100%) rename test/{ => sub_client}/test_client_faulty_response.py (100%) rename test/{ => sub_client}/test_client_sync.py (100%) rename test/{ => sub_server}/test_server_asyncio.py (100%) rename test/{ => sub_server}/test_server_context.py (100%) rename test/{ => sub_server}/test_server_multidrop.py (100%) diff --git a/examples/client_async.py b/examples/client_async.py index 6af8cde366..2082eec3c8 100755 --- a/examples/client_async.py +++ b/examples/client_async.py @@ -58,7 +58,7 @@ def setup_async_client(description=None, cmdline=None): port=args.port, # on which port # Common optional paramers: framer=args.framer, - timeout=5, + timeout=args.timeout, retries=3, reconnect_delay=1, reconnect_delay_max=10, @@ -74,7 +74,7 @@ def setup_async_client(description=None, cmdline=None): port=args.port, # Common optional paramers: framer=args.framer, - # timeout=10, + timeout=args.timeout, # retries=3, # retry_on_empty=False, # close_comm_on_error=False, @@ -87,7 +87,7 @@ def setup_async_client(description=None, cmdline=None): args.port, # Common optional paramers: # framer=ModbusRtuFramer, - # timeout=10, + timeout=args.timeout, # retries=3, # retry_on_empty=False, # close_comm_on_error=False, @@ -105,7 +105,7 @@ def setup_async_client(description=None, cmdline=None): port=args.port, # Common optional paramers: framer=args.framer, - # timeout=10, + timeout=args.timeout, # retries=3, # retry_on_empty=False, # close_comm_on_error=False, diff --git a/examples/client_sync.py b/examples/client_sync.py index bf4c4b136c..3874507dcc 100755 --- a/examples/client_sync.py +++ b/examples/client_sync.py @@ -58,7 +58,7 @@ def setup_sync_client(description=None, cmdline=None): port=args.port, # Common optional paramers: framer=args.framer, - # timeout=10, + timeout=args.timeout, # retries=3, # retry_on_empty=False,y # close_comm_on_error=False, @@ -72,7 +72,7 @@ def setup_sync_client(description=None, cmdline=None): port=args.port, # Common optional paramers: framer=args.framer, - # timeout=10, + timeout=args.timeout, # retries=3, # retry_on_empty=False, # close_comm_on_error=False, @@ -85,7 +85,7 @@ def setup_sync_client(description=None, cmdline=None): port=args.port, # serial port # Common optional paramers: # framer=ModbusRtuFramer, - # timeout=10, + timeout=args.timeout, # retries=3, # retry_on_empty=False, # close_comm_on_error=False,. @@ -103,7 +103,7 @@ def setup_sync_client(description=None, cmdline=None): port=args.port, # Common optional paramers: framer=args.framer, - # timeout=10, + timeout=args.timeout, # retries=3, # retry_on_empty=False, # close_comm_on_error=False, diff --git a/examples/datastore_simulator.py b/examples/datastore_simulator.py index 34036af03c..121f682d1e 100755 --- a/examples/datastore_simulator.py +++ b/examples/datastore_simulator.py @@ -6,6 +6,7 @@ usage: server_simulator.py [-h] [--log {critical,error,warning,info,debug}] [--port PORT] + [--host HOST] Command line options for examples @@ -14,6 +15,7 @@ --log {critical,error,warning,info,debug} "critical", "error", "warning", "info" or "debug" --port PORT the port to use + --host HOST the interface to listen on The corresponding client can be started as: python3 client_sync.py @@ -129,6 +131,7 @@ def get_commandline(cmdline=None): type=str, ) parser.add_argument("--port", help="set port", type=str, default="5020") + parser.add_argument("--host", help="set interface", type=str, default="localhost") args = parser.parse_args(cmdline) return args @@ -168,7 +171,7 @@ async def run_server_simulator(args): await StartAsyncTcpServer( context=args.context, - address=("", args.port), + address=(args.host, args.port), framer=args.framer, ) diff --git a/examples/helper.py b/examples/helper.py index 6e6f226b11..fd634ceaa1 100755 --- a/examples/helper.py +++ b/examples/helper.py @@ -21,6 +21,18 @@ _logger = logging.getLogger(__file__) +def get_framer(framer): + """Convert framer name to framer class""" + framers = { + "ascii": ModbusAsciiFramer, + "binary": ModbusBinaryFramer, + "rtu": ModbusRtuFramer, + "socket": ModbusSocketFramer, + "tls": ModbusTlsFramer, + } + return framers[framer] + + def get_commandline(server=False, description=None, extras=None, cmdline=None): """Read and validate command line arguments""" parser = argparse.ArgumentParser(description=description) @@ -90,6 +102,13 @@ def get_commandline(server=False, description=None, extras=None, cmdline=None): help="ADVANCED USAGE: set datastore context object", default=None, ) + else: + parser.add_argument( + "--timeout", + help="ADVANCED USAGE: set client timeout", + default=10, + type=float, + ) if extras: # pragma no cover for extra in extras: parser.add_argument(extra[0], **extra[1]) @@ -102,16 +121,9 @@ def get_commandline(server=False, description=None, extras=None, cmdline=None): "serial": ["rtu", "/dev/ptyp0"], "tls": ["tls", 5020], } - framers = { - "ascii": ModbusAsciiFramer, - "binary": ModbusBinaryFramer, - "rtu": ModbusRtuFramer, - "socket": ModbusSocketFramer, - "tls": ModbusTlsFramer, - } pymodbus_apply_logging_config(args.log.upper()) _logger.setLevel(args.log.upper()) - args.framer = framers[args.framer or comm_defaults[args.comm][0]] + args.framer = get_framer(args.framer or comm_defaults[args.comm][0]) args.port = args.port or comm_defaults[args.comm][1] if args.comm != "serial" and args.port: args.port = int(args.port) diff --git a/examples/simple_async_client.py b/examples/simple_async_client.py index 65306b9f40..aa02bd621f 100755 --- a/examples/simple_async_client.py +++ b/examples/simple_async_client.py @@ -33,7 +33,7 @@ ) -async def run_async_simple_client(comm, host, port): +async def run_async_simple_client(comm, host, port, framer=ModbusSocketFramer): """Run async client.""" # activate debugging @@ -44,7 +44,7 @@ async def run_async_simple_client(comm, host, port): client = AsyncModbusTcpClient( host, port=port, - framer=ModbusSocketFramer, + framer=framer, # timeout=10, # retries=3, # retry_on_empty=False, diff --git a/examples/simple_sync_client.py b/examples/simple_sync_client.py index 4ac99c12cc..3dd4184e71 100755 --- a/examples/simple_sync_client.py +++ b/examples/simple_sync_client.py @@ -31,7 +31,7 @@ ) -def run_sync_simple_client(comm, host, port): +def run_sync_simple_client(comm, host, port, framer=ModbusSocketFramer): """Run sync client.""" # activate debugging @@ -42,7 +42,7 @@ def run_sync_simple_client(comm, host, port): client = ModbusTcpClient( host, port=port, - framer=ModbusSocketFramer, + framer=framer, # timeout=10, # retries=3, # retry_on_empty=False,y diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 8d23cf726a..a99d247e80 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -88,7 +88,7 @@ def __init__( # pylint: disable=too-many-arguments reconnect_delay_max=reconnect_delay_max, timeout_connect=timeout, host=kwargs.get("host", None), - port=kwargs.get("port", None), + port=kwargs.get("port", 0), sslctx=kwargs.get("sslctx", None), baudrate=kwargs.get("baudrate", None), bytesize=kwargs.get("bytesize", None), diff --git a/pymodbus/server/async_io.py b/pymodbus/server/async_io.py index 45796b4d13..02d750c8c8 100644 --- a/pymodbus/server/async_io.py +++ b/pymodbus/server/async_io.py @@ -1,6 +1,7 @@ """Implementation of a Threaded Modbus Server.""" # pylint: disable=missing-type-doc import asyncio +import os import time import traceback from contextlib import suppress @@ -654,7 +655,10 @@ async def async_stop(cls): if not cls.active_server: raise RuntimeError("ServerAsyncStop called without server task active.") await cls.active_server.server.shutdown() - await asyncio.sleep(1) + if os.name == "nt": + await asyncio.sleep(1) + else: + await asyncio.sleep(0.1) cls.active_server = None @classmethod @@ -667,7 +671,10 @@ def stop(cls): Log.info("ServerStop called with loop stopped.") return asyncio.run_coroutine_threadsafe(cls.async_stop(), cls.active_server.loop) - time.sleep(10) + if os.name == "nt": + time.sleep(10) + else: + time.sleep(0.5) async def StartAsyncTcpServer( # pylint: disable=invalid-name,dangerous-default-value diff --git a/pymodbus/transport/transport.py b/pymodbus/transport/transport.py index 6993b0f02d..50c232e472 100644 --- a/pymodbus/transport/transport.py +++ b/pymodbus/transport/transport.py @@ -1,4 +1,51 @@ -"""ModbusProtocol layer.""" +"""ModbusProtocol layer. + +Contains pure transport methods needed to +- connect/listen, +- send/receive +- close/abort connections +for unix socket, tcp, tls and serial communications as well as a special +null modem option. + +Contains high level methods like reconnect. + +All transport differences are handled in transport, providing a unified +interface to upper layers. + +Host/Port/SourceAddress explanation: +- SourceAddress (host, port): +- server (host, port): Listen on host:port +- server serial (comm_port, _): comm_port is device string +- client (host, port): Bind host:port to interface +- client serial: not used +- Host +- server: not used +- client: remote host to connect to (as host:port) +- client serial: host is comm_port device string +- Port +- server: not used +- client: remote port to connect to (as host:port) +- client serial: no used + +Pyserial allow the comm_port to be a socket e.g. "socket://localhost:502", +this allows serial clients to connect to a tcp server with RTU framer. + +Pymodbus allows this format for both server and client. +For clients the string is passed to pyserial, +but for servers it is used to start a modbus tcp server. +This allows for serial testing, without a serial cable. + +Pymodbus offers nullmodem for clients/servers running in the same process +if is set to NULLMODEM_HOST it will be automatically invoked. +This allows testing without actual network traffic and is a lot faster. + +Class NullModem is a asyncio transport class, +that replaces the socket class or pyserial. + +The class is designed to take care of differences between the different +transport mediums, and provide a neutral interface for the upper layers. +It basically provides a pipe, without caring about the actual data content. +""" from __future__ import annotations import asyncio @@ -18,7 +65,7 @@ if sys.version_info.minor == 11: USEEXCEPTIONS: tuple[type[Any], type[Any]] | type[Any] = OSError else: - USEEXCEPTIONS = ( + USEEXCEPTIONS = ( # pragma: no cover asyncio.TimeoutError, OSError, ) @@ -93,30 +140,7 @@ def copy(self): class ModbusProtocol(asyncio.BaseProtocol): - """Protocol layer including transport. - - Contains pure transport methods needed to connect/listen, send/receive and close connections - for unix socket, tcp, tls and serial communications. - - Contains high level methods like reconnect. - - Host/Port/SourceAddress explanation: - - SourceAddress: - - server: (host, port) to listen on (default is ("127.0.0.1", 502/802)) - - server serial: (host, _) to open/connect and listen on - - client: (Bind local part to interface (default is local interface) - - client serial: (host, _) to open/connect and listen on - - Host - - Server: not used - - Client serial: port string to use for connecting - - Client others: remote host to connect to - - Port - - Server/Client serial: not used - - Client others: remote port to connect to - - The class is designed to take care of differences between the different transport mediums, and - provide a neutral interface for the upper layers. - """ + """Protocol layer including transport.""" def __init__( self, @@ -146,74 +170,63 @@ def __init__( self.sent_buffer: bytes = b"" # ModbusProtocol specific setup - if self.comm_params.comm_type == CommType.SERIAL: - self.init_correct_serial() - if self.init_check_nullmodem(): - return - self.init_setup_connect_listen() - - def init_correct_serial(self) -> None: - """Split host for serial if needed.""" if self.is_server: host = self.comm_params.source_address[0] - if host.startswith("socket"): - parts = host[9:].split(":") - self.comm_params.source_address = (parts[0], int(parts[1])) - self.comm_params.comm_type = CommType.TCP - elif host.startswith(NULLMODEM_HOST): - self.comm_params.source_address = (host, int(host[9:].split(":")[1])) - return - if self.comm_params.host.startswith(NULLMODEM_HOST): - self.comm_params.port = int(self.comm_params.host[9:].split(":")[1]) - - def init_check_nullmodem(self) -> bool: - """Check if nullmodem is needed.""" - if self.comm_params.host.startswith(NULLMODEM_HOST): - port = self.comm_params.port - elif self.comm_params.source_address[0].startswith(NULLMODEM_HOST): - port = self.comm_params.source_address[1] + port = int(self.comm_params.source_address[1]) else: - return False + host = self.comm_params.host + port = int(self.comm_params.port) + if self.comm_params.comm_type == CommType.SERIAL: + host, port = self.init_setup_serial(host, port) + if not host and not port: + return + if host == NULLMODEM_HOST: + self.call_create = lambda: self.create_nullmodem(port) + return + # TCP/TLS/UDP + self.init_setup_connect_listen(host, port) - self.call_create = lambda: self.create_nullmodem(port) - return True + def init_setup_serial(self, host: str, _port: int) -> tuple[str, int]: + """Split host for serial if needed.""" + if NULLMODEM_HOST in host: + return NULLMODEM_HOST, int(host[9:].split(":")[1]) + if self.is_server and host.startswith("socket"): + # format is "socket://:port" + self.comm_params.comm_type = CommType.TCP + parts = host.split(":") + return parts[1][2:], int(parts[2]) + self.call_create = lambda: create_serial_connection( + self.loop, + self.handle_new_connection, + host, + baudrate=self.comm_params.baudrate, + bytesize=self.comm_params.bytesize, + parity=self.comm_params.parity, + stopbits=self.comm_params.stopbits, + timeout=self.comm_params.timeout_connect, + ) + return None, None - def init_setup_connect_listen(self) -> None: + def init_setup_connect_listen(self, host: str, port: int) -> None: """Handle connect/listen handler.""" - if self.comm_params.comm_type == CommType.SERIAL: - if self.is_server: - host = self.comm_params.source_address[0] - else: - host = self.comm_params.host - self.call_create = lambda: create_serial_connection( - self.loop, - self.handle_new_connection, - host, - baudrate=self.comm_params.baudrate, - bytesize=self.comm_params.bytesize, - parity=self.comm_params.parity, - stopbits=self.comm_params.stopbits, - timeout=self.comm_params.timeout_connect, - ) - return if self.comm_params.comm_type == CommType.UDP: if self.is_server: self.call_create = lambda: self.loop.create_datagram_endpoint( self.handle_new_connection, - local_addr=self.comm_params.source_address, + local_addr=(host, port), ) else: self.call_create = lambda: self.loop.create_datagram_endpoint( self.handle_new_connection, - remote_addr=(self.comm_params.host, self.comm_params.port), + remote_addr=(host, port), ) return # TLS and TCP if self.is_server: self.call_create = lambda: self.loop.create_server( self.handle_new_connection, - self.comm_params.source_address[0], - self.comm_params.source_address[1], + host, + port, ssl=self.comm_params.sslctx, reuse_address=True, start_serving=True, @@ -221,9 +234,9 @@ def init_setup_connect_listen(self) -> None: else: self.call_create = lambda: self.loop.create_connection( self.handle_new_connection, - self.comm_params.host, - self.comm_params.port, - # local_addr=self.comm_params.source_address, + host, + port, + local_addr=self.comm_params.source_address, ssl=self.comm_params.sslctx, ) diff --git a/test/conftest.py b/test/conftest.py index 5d1b321b32..e4da6bccf0 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -5,6 +5,7 @@ import pytest from pymodbus.datastore import ModbusBaseSlaveContext +from pymodbus.transport import NullModem def pytest_configure(): @@ -17,14 +18,18 @@ def pytest_configure(): # Generic fixtures # -----------------------------------------------------------------------# BASE_PORTS = { - "TestBasicModbusProtocol": 8100, - "TestBasicSerial": 8200, - "TestCommModbusProtocol": 8300, - "TestCommNullModem": 8400, - "TestExamples": 8500, - "TestModbusProtocol": 8600, - "TestNullModem": 8700, - "TestReconnectModbusProtocol": 8800, + "TestBasicModbusProtocol": 7100, + "TestBasicSerial": 7200, + "TestCommModbusProtocol": 7300, + "TestCommNullModem": 7400, + "TestExamples": 7500, + "TestAsyncExamples": 7600, + "TestSyncExamples": 7700, + "TestModbusProtocol": 7800, + "TestNullModem": 7900, + "TestReconnectModbusProtocol": 8000, + "TestClientServerSyncExamples": 8100, + "TestClientServerAsyncExamples": 8200, } @@ -34,6 +39,13 @@ def get_base_ports(): return BASE_PORTS +@pytest.fixture(name="nullmodem_check", autouse=True) +def _check_nullmodem(): + """Check null modem.""" + yield + assert not NullModem.is_dirty() + + class MockContext(ModbusBaseSlaveContext): """Mock context.""" diff --git a/test/test_client.py b/test/sub_client/test_client.py similarity index 100% rename from test/test_client.py rename to test/sub_client/test_client.py diff --git a/test/test_client_faulty_response.py b/test/sub_client/test_client_faulty_response.py similarity index 100% rename from test/test_client_faulty_response.py rename to test/sub_client/test_client_faulty_response.py diff --git a/test/test_client_sync.py b/test/sub_client/test_client_sync.py similarity index 100% rename from test/test_client_sync.py rename to test/sub_client/test_client_sync.py diff --git a/test/sub_examples/conftest.py b/test/sub_examples/conftest.py index 73016b3479..470b054db6 100644 --- a/test/sub_examples/conftest.py +++ b/test/sub_examples/conftest.py @@ -9,12 +9,6 @@ from pymodbus.transport import NULLMODEM_HOST -@pytest.fixture(name="port_offset") -def define_port_offset(): - """Define port offset""" - return 0 - - @pytest.fixture(name="use_host") def define_use_host(): """Set default host""" @@ -27,16 +21,10 @@ def define_commandline_client( use_framer, use_port, use_host, - port_offset, ): """Define commandline.""" - my_port = str(use_port + port_offset) - cmdline = [ - "--comm", - use_comm, - "--framer", - use_framer, - ] + my_port = str(use_port) + cmdline = ["--comm", use_comm, "--framer", use_framer, "--timeout", "0.1"] if use_comm == "serial": if use_host == NULLMODEM_HOST: use_host = f"{use_host}:{my_port}" @@ -54,10 +42,9 @@ def define_commandline_server( use_framer, use_port, use_host, - port_offset, ): """Define commandline.""" - my_port = str(use_port + port_offset) + my_port = str(use_port) cmdline = [ "--comm", use_comm, diff --git a/test/sub_examples/test_client_server_async.py b/test/sub_examples/test_client_server_async.py index d3ac55977c..6431130078 100755 --- a/test/sub_examples/test_client_server_async.py +++ b/test/sub_examples/test_client_server_async.py @@ -21,38 +21,32 @@ from pymodbus.exceptions import ModbusIOException -BASE_PORT = 6200 - - +@pytest.mark.parametrize( + ("use_comm", "use_framer"), + [ + ("tcp", "socket"), + ("tcp", "rtu"), + ("tls", "tls"), + ("udp", "socket"), + ("udp", "rtu"), + ("serial", "rtu"), + ], +) class TestClientServerAsyncExamples: """Test Client server async examples.""" - USE_CASES = [ - ("tcp", "socket", BASE_PORT + 1), - ("tcp", "rtu", BASE_PORT + 2), - ("tls", "tls", BASE_PORT + 3), - ("udp", "socket", BASE_PORT + 4), - ("udp", "rtu", BASE_PORT + 5), - ("serial", "rtu", BASE_PORT + 6), - # awaiting fix: ("serial", "ascii", BASE_PORT + 7), - # awaiting fix: ("serial", "binary", BASE_PORT + 8), - ] + @staticmethod + @pytest.fixture(name="use_port") + def get_port_in_class(base_ports): + """Return next port""" + base_ports[__class__.__name__] += 1 + return base_ports[__class__.__name__] - @pytest.mark.parametrize("port_offset", [0]) - @pytest.mark.parametrize( - ("use_comm", "use_framer", "use_port"), - USE_CASES, - ) async def test_combinations(self, mock_server, mock_clc): """Run async client and server.""" assert mock_server await main(cmdline=mock_clc) - @pytest.mark.parametrize("port_offset", [1]) - @pytest.mark.parametrize( - ("use_comm", "use_framer", "use_port"), - [("tcp", "socket", BASE_PORT + 1)], - ) async def test_client_exception(self, mock_server, mock_clc): """Run async client and server.""" assert mock_server @@ -62,20 +56,10 @@ async def test_client_exception(self, mock_server, mock_clc): ) await run_async_client(test_client, modbus_calls=run_a_few_calls) - @pytest.mark.parametrize("port_offset", [10]) - @pytest.mark.parametrize( - ("use_comm", "use_framer", "use_port"), - USE_CASES, - ) async def test_server_no_client(self, mock_server): """Run async server without client.""" assert mock_server - @pytest.mark.parametrize("port_offset", [20]) - @pytest.mark.parametrize( - ("use_comm", "use_framer", "use_port"), - USE_CASES, - ) async def test_server_client_twice(self, mock_server, use_comm, mock_clc): """Run async server without client.""" assert mock_server @@ -86,11 +70,6 @@ async def test_server_client_twice(self, mock_server, use_comm, mock_clc): await asyncio.sleep(0.5) await run_async_client(test_client, modbus_calls=run_a_few_calls) - @pytest.mark.parametrize("port_offset", [30]) - @pytest.mark.parametrize( - ("use_comm", "use_framer", "use_port"), - USE_CASES, - ) async def test_client_no_server(self, mock_clc): """Run async client without server.""" test_client = setup_async_client(cmdline=mock_clc) diff --git a/test/sub_examples/test_client_server_sync.py b/test/sub_examples/test_client_server_sync.py index 40debbdbe7..8bacb9de6d 100755 --- a/test/sub_examples/test_client_server_sync.py +++ b/test/sub_examples/test_client_server_sync.py @@ -7,6 +7,7 @@ These are basis for most examples and thus tested separately """ +import os from threading import Thread from time import sleep @@ -24,29 +25,34 @@ from pymodbus.server import ServerStop -BASE_PORT = 6300 +if os.name == "nt": + SLEEPING = 1 +else: + SLEEPING = 0.1 +@pytest.mark.parametrize("use_host", ["localhost"]) +@pytest.mark.parametrize( + ("use_comm", "use_framer"), + [ + ("tcp", "socket"), + ("tcp", "rtu"), + # awaiting fix: ("tls", "tls"), + ("udp", "socket"), + ("udp", "rtu"), + ("serial", "rtu"), + ], +) class TestClientServerSyncExamples: """Test Client server async combinations.""" - USE_CASES = [ - ("tcp", "socket", BASE_PORT + 1), - ("tcp", "rtu", BASE_PORT + 2), - # awaiting fix: ("tls", "tls", BASE_PORT + 3), - ("udp", "socket", BASE_PORT + 4), - ("udp", "rtu", BASE_PORT + 5), - ("serial", "rtu", BASE_PORT + 6), - # awaiting fix: ("serial", "ascii", BASE_PORT + 7), - # awaiting fix: ("serial", "binary", BASE_PORT + 8), - ] + @staticmethod + @pytest.fixture(name="use_port") + def get_port_in_class(base_ports): + """Return next port""" + base_ports[__class__.__name__] += 1 + return base_ports[__class__.__name__] - @pytest.mark.parametrize("port_offset", [0]) - @pytest.mark.parametrize("use_host", ["localhost"]) - @pytest.mark.parametrize( - ("use_comm", "use_framer", "use_port"), - USE_CASES, - ) def test_combinations( self, mock_clc, @@ -57,52 +63,35 @@ def test_combinations( thread = Thread(target=run_sync_server, args=(server_args,)) thread.daemon = True thread.start() - sleep(1) + sleep(SLEEPING) main(cmdline=mock_clc) ServerStop() - @pytest.mark.parametrize("port_offset", [10]) - @pytest.mark.parametrize("use_host", ["localhost"]) - @pytest.mark.parametrize( - ("use_comm", "use_framer", "use_port"), - USE_CASES, - ) def test_server_no_client(self, mock_cls): """Run async server without client.""" server_args = setup_server(cmdline=mock_cls) thread = Thread(target=run_sync_server, args=(server_args,)) thread.daemon = True thread.start() - sleep(1) + sleep(SLEEPING) ServerStop() - @pytest.mark.parametrize("port_offset", [20]) - @pytest.mark.parametrize("use_host", ["localhost"]) - @pytest.mark.parametrize( - ("use_comm", "use_framer", "use_port"), - USE_CASES, - ) def test_server_client_twice(self, mock_cls, mock_clc, use_comm): """Run async server without client.""" if use_comm == "serial": + # cannot open the usb port multiple times return server_args = setup_server(cmdline=mock_cls) thread = Thread(target=run_sync_server, args=(server_args,)) thread.daemon = True thread.start() - sleep(1) + sleep(SLEEPING) test_client = setup_sync_client(cmdline=mock_clc) run_sync_client(test_client, modbus_calls=run_a_few_calls) - sleep(0.5) + sleep(SLEEPING) run_sync_client(test_client, modbus_calls=run_a_few_calls) ServerStop() - @pytest.mark.parametrize("port_offset", [30]) - @pytest.mark.parametrize("use_host", ["localhost"]) - @pytest.mark.parametrize( - ("use_comm", "use_framer", "use_port"), - USE_CASES, - ) def test_client_no_server(self, mock_clc): """Run async client without server.""" if mock_clc[1] == "udp": diff --git a/test/sub_examples/test_examples.py b/test/sub_examples/test_examples.py index bfc8f98577..3e03d04c4b 100755 --- a/test/sub_examples/test_examples.py +++ b/test/sub_examples/test_examples.py @@ -18,6 +18,7 @@ from examples.client_custom_msg import main as main_custom_client from examples.client_payload import main as main_payload_calls from examples.datastore_simulator import main as main_datastore_simulator +from examples.helper import get_framer from examples.message_generator import generate_messages from examples.message_parser import main as main_parse_messages from examples.server_async import setup_server @@ -31,7 +32,6 @@ from pymodbus.exceptions import ModbusException from pymodbus.pdu import ExceptionResponse from pymodbus.server import ServerAsyncStop, ServerStop -from pymodbus.transport import NullModem class TestExamples: @@ -39,50 +39,96 @@ class TestExamples: @staticmethod @pytest.fixture(name="use_port") - def get_my_port(base_ports): + def get_port_in_class(base_ports): """Return next port""" base_ports[__class__.__name__] += 1 return base_ports[__class__.__name__] - def teardown(self): - """Run class teardown""" - assert not NullModem.is_dirty() + @pytest.mark.parametrize("framer", ["socket", "rtu", "ascii", "binary"]) + def test_message_generator(self, framer): + """Test all message generator.""" + generate_messages(cmdline=["--framer", framer]) + + @pytest.mark.parametrize("framer", ["socket", "rtu", "ascii", "binary"]) + def test_message_parser(self, framer): + """Test message parser.""" + main_parse_messages(["--framer", framer, "-m", "000100000006010100200001"]) + main_parse_messages(["--framer", framer, "-m", "00010000000401010101"]) + + async def test_server_callback(self, use_port, use_host): + """Test server/client with payload.""" + cmdargs = ["--port", str(use_port), "--host", use_host] + task = asyncio.create_task(run_callback_server(cmdline=cmdargs)) + await asyncio.sleep(0.1) + testclient = setup_async_client(cmdline=cmdargs) + await run_async_client(testclient, modbus_calls=run_a_few_calls) + await asyncio.sleep(0.1) + await ServerAsyncStop() + await asyncio.sleep(0.1) + task.cancel() + await task - USE_CASES = [ + async def test_updating_server(self, use_port, use_host): + """Test server simulator.""" + cmdargs = ["--port", str(use_port), "--host", use_host] + task = asyncio.create_task(main_updating_server(cmdline=cmdargs)) + await asyncio.sleep(0.1) + client = setup_async_client(cmdline=cmdargs) + await run_async_client(client, modbus_calls=run_a_few_calls) + await asyncio.sleep(0.1) + await ServerAsyncStop() + await asyncio.sleep(0.1) + task.cancel() + await task + + async def test_datastore_simulator(self, use_port, use_host): + """Test server simulator.""" + cmdargs = ["--port", str(use_port), "--host", use_host] + task = asyncio.create_task(main_datastore_simulator(cmdline=cmdargs)) + await asyncio.sleep(0.1) + testclient = setup_async_client(cmdline=cmdargs) + await run_async_client(testclient, modbus_calls=run_a_few_calls) + await asyncio.sleep(0.1) + await ServerAsyncStop() + await asyncio.sleep(0.1) + task.cancel() + await task + + async def xtest_simulator(self): + """Run simulator server/client.""" + # Awaiting fix, missing stop of task. + await run_simulator() + + async def test_modbus_forwarder(self): + """Test modbus forwarder.""" + print("waiting for fix") + + +@pytest.mark.parametrize( + ("use_comm", "use_framer"), + [ ("tcp", "socket"), ("tcp", "rtu"), ("tls", "tls"), ("udp", "socket"), ("udp", "rtu"), ("serial", "rtu"), - # awaiting fix: ("serial", "ascii", BASE_PORT + 7), - # awaiting fix: ("serial", "binary", BASE_PORT + 8), - ] - - @pytest.mark.parametrize("framer", ["socket", "rtu", "ascii", "binary"]) - def test_message_generator(self, framer): - """Test all message generator.""" - generate_messages(cmdline=["--framer", framer]) + ], +) +class TestAsyncExamples: + """Test examples.""" - def test_message_parser(self): - """Test message parser.""" - main_parse_messages(["--framer", "socket", "-m", "000100000006010100200001"]) - main_parse_messages(["--framer", "socket", "-m", "00010000000401010101"]) + @staticmethod + @pytest.fixture(name="use_port") + def get_port_in_class(base_ports): + """Return next port""" + base_ports[__class__.__name__] += 1 + return base_ports[__class__.__name__] - @pytest.mark.parametrize( - ("use_comm", "use_framer"), - USE_CASES, - ) async def test_client_async_calls(self, mock_server): """Test client_async_calls.""" await main_client_async_calls(cmdline=mock_server) - @pytest.mark.parametrize( - ("use_comm", "use_framer"), - [ - ("tcp", "socket"), - ], - ) async def test_client_async_calls_errors(self, mock_server): """Test client_async_calls.""" client = setup_async_client(cmdline=mock_server) @@ -95,36 +141,6 @@ async def test_client_async_calls_errors(self, mock_server): await run_async_client(client, modbus_calls=async_template_call) client.close() - @pytest.mark.parametrize("use_host", ["localhost"]) - @pytest.mark.parametrize( - ("use_comm", "use_framer"), - [ - ("tcp", "socket"), - ("tcp", "rtu"), - # awaiting fix: ("tls", "tls", BASE_PORT + 3), - ("udp", "socket"), - ("udp", "rtu"), - ("serial", "rtu"), - # awaiting fix: ("serial", "ascii", BASE_PORT + 7), - # awaiting fix: ("serial", "binary", BASE_PORT + 8), - ], - ) - def test_client_calls(self, mock_clc, mock_cls): - """Test client_calls.""" - server_args = setup_server(cmdline=mock_cls) - thread = Thread(target=run_sync_server, args=(server_args,)) - thread.daemon = True - thread.start() - sleep(1) - main_client_calls(cmdline=mock_clc) - ServerStop() - - @pytest.mark.parametrize( - ("use_comm", "use_framer"), - [ - ("tcp", "socket"), - ], - ) async def test_client_calls_errors(self, mock_server): """Test client_calls.""" client = setup_async_client(cmdline=mock_server) @@ -137,24 +153,15 @@ async def test_client_calls_errors(self, mock_server): await run_async_client(client, modbus_calls=template_call) client.close() - @pytest.mark.parametrize("use_host", ["localhost"]) - @pytest.mark.parametrize( - ("use_comm", "use_framer"), - [ - ("tcp", "socket"), - ], - ) - async def test_custom_msg(self, mock_server, use_port, use_host): + async def test_custom_msg( + self, mock_server, use_comm, use_framer, use_port, use_host + ): """Test client with custom message.""" + if use_comm != "tcp" or use_framer != "socket": + return assert mock_server await main_custom_client(port=use_port, host=use_host) - @pytest.mark.parametrize( - ("use_comm", "use_framer"), - [ - ("tcp", "socket"), - ], - ) async def test_payload(self, mock_clc, mock_cls): """Test server/client with payload.""" task = asyncio.create_task(main_payload_server(cmdline=mock_cls)) @@ -166,86 +173,59 @@ async def test_payload(self, mock_clc, mock_cls): task.cancel() await task - async def test_datastore_simulator(self, use_port): - """Test server simulator.""" - cmdargs = ["--port", str(use_port)] - task = asyncio.create_task( - main_datastore_simulator(cmdline=["--port", str(use_port)]) - ) - await asyncio.sleep(0.1) - cmdargs.extend(["--host", "localhost"]) - testclient = setup_async_client(cmdline=cmdargs) - await run_async_client(testclient, modbus_calls=run_a_few_calls) - await asyncio.sleep(0.1) - await ServerAsyncStop() - await asyncio.sleep(0.1) - task.cancel() - await task - - async def test_server_callback(self, use_port): - """Test server/client with payload.""" - cmdargs = ["--port", str(use_port)] - task = asyncio.create_task(run_callback_server(cmdline=cmdargs)) - await asyncio.sleep(0.1) - testclient = setup_async_client(cmdline=cmdargs) - await run_async_client(testclient, modbus_calls=run_a_few_calls) - await asyncio.sleep(0.1) - await ServerAsyncStop() - await asyncio.sleep(0.1) - task.cancel() - await task - - async def test_updating_server(self, use_port): - """Test server simulator.""" - cmdargs = ["--port", str(use_port)] - task = asyncio.create_task(main_updating_server(cmdline=cmdargs)) - await asyncio.sleep(0.1) - client = setup_async_client(cmdline=cmdargs) - await run_async_client(client, modbus_calls=run_a_few_calls) - await asyncio.sleep(0.1) - await ServerAsyncStop() - await asyncio.sleep(0.1) - task.cancel() - await task - - @pytest.mark.parametrize("use_host", ["localhost"]) - @pytest.mark.parametrize( - ("use_comm", "use_framer"), - [ - ("tcp", "socket"), - # awaiting fix ("tls", "tls", BASE_PORT + 47), - ("udp", "socket"), - ("serial", "rtu"), - ], - ) - async def test_async_simple_client(self, use_comm, use_port, mock_server, use_host): + async def test_async_simple_client( + self, use_comm, use_port, use_framer, mock_server, use_host + ): """Run simple async client.""" _cmdline = mock_server + if use_comm == "tls": + return + if use_comm == "udp" and use_framer == "rtu": + return if use_comm == "serial": use_port = f"socket://{use_host}:{use_port}" - await run_async_simple_client(use_comm, use_host, use_port) - - @pytest.mark.parametrize("use_host", ["localhost"]) - @pytest.mark.parametrize( - ("use_comm", "use_framer"), - [ - ("tcp", "socket"), - # awaiting fix ("tls", "tls", BASE_PORT + 47), - ("udp", "socket"), - ("serial", "rtu"), - ], - ) - async def test_sync_simple_client(self, use_comm, use_host, use_port, mock_server): + framer = get_framer(use_framer) + await run_async_simple_client(use_comm, use_host, use_port, framer=framer) + + +@pytest.mark.parametrize("use_host", ["localhost"]) +@pytest.mark.parametrize( + ("use_comm", "use_framer"), + [ + ("tcp", "socket"), + ("tcp", "rtu"), + # awaiting fix: ("tls", "tls"), + ("udp", "socket"), + ("udp", "rtu"), + ("serial", "rtu"), + ], +) +class TestSyncExamples: + """Test examples.""" + + @staticmethod + @pytest.fixture(name="use_port") + def get_port_in_class(base_ports): + """Return next port""" + base_ports[__class__.__name__] += 1 + return base_ports[__class__.__name__] + + def test_client_calls(self, mock_clc, mock_cls): + """Test client_calls.""" + server_args = setup_server(cmdline=mock_cls) + thread = Thread(target=run_sync_server, args=(server_args,)) + thread.daemon = True + thread.start() + sleep(1) + main_client_calls(cmdline=mock_clc) + ServerStop() + + async def test_sync_simple_client( + self, use_comm, use_host, use_port, use_framer, mock_server + ): """Run simple async client.""" _cmdline = mock_server if use_comm == "serial": use_port = f"socket://{use_host}:{use_port}" - run_sync_simple_client(use_comm, use_host, use_port) - - async def test_simulator(self): - """Run simulator server/client.""" - await run_simulator() - - async def test_modbus_forwarder(self): - """Test modbus forwarder.""" - print("waiting for fix") + framer = get_framer(use_framer) + run_sync_simple_client(use_comm, use_host, use_port, framer=framer) diff --git a/test/test_server_asyncio.py b/test/sub_server/test_server_asyncio.py similarity index 100% rename from test/test_server_asyncio.py rename to test/sub_server/test_server_asyncio.py diff --git a/test/test_server_context.py b/test/sub_server/test_server_context.py similarity index 100% rename from test/test_server_context.py rename to test/sub_server/test_server_context.py diff --git a/test/test_server_multidrop.py b/test/sub_server/test_server_multidrop.py similarity index 100% rename from test/test_server_multidrop.py rename to test/sub_server/test_server_multidrop.py diff --git a/test/sub_transport/conftest.py b/test/sub_transport/conftest.py index e810bcdeb4..be2c8e20bc 100644 --- a/test/sub_transport/conftest.py +++ b/test/sub_transport/conftest.py @@ -63,9 +63,9 @@ def prepare_dummy_use_comm_type(): @pytest.fixture(name="use_host") -def prepare_dummy_use_host(): +def prepare_nullmodem_host(): """Return default host""" - return "localhost" + return NULLMODEM_HOST @pytest.fixture(name="use_cls") @@ -92,12 +92,12 @@ def prepare_commparams_client(use_port, use_host, use_comm_type): """Prepare CommParamsClass object.""" if use_host == NULLMODEM_HOST and use_comm_type == CommType.SERIAL: use_host = f"{NULLMODEM_HOST}:{use_port}" - timeout = 10 if not pytest.IS_WINDOWS else 5 + timeout = 10 if not pytest.IS_WINDOWS else 2 return CommParams( comm_name="test comm", comm_type=use_comm_type, - reconnect_delay=1, - reconnect_delay_max=3.5, + reconnect_delay=0.1, + reconnect_delay_max=0.35, timeout_connect=timeout, host=use_host, port=use_port, diff --git a/test/sub_transport/test_basic.py b/test/sub_transport/test_basic.py index 8a05ee999a..05765ee129 100644 --- a/test/sub_transport/test_basic.py +++ b/test/sub_transport/test_basic.py @@ -5,7 +5,6 @@ import pytest from pymodbus.transport import ( - NULLMODEM_HOST, CommType, ModbusProtocol, NullModem, @@ -29,16 +28,11 @@ class TestBasicModbusProtocol: @staticmethod @pytest.fixture(name="use_port") - def get_my_port(base_ports): + def get_port_in_class(base_ports): """Return next port""" base_ports[__class__.__name__] += 1 return base_ports[__class__.__name__] - def teardown(self): - """Run class teardown""" - assert not NullModem.is_dirty() - - @pytest.mark.parametrize("use_host", [NULLMODEM_HOST]) @pytest.mark.parametrize("use_comm_type", COMM_TYPES) async def test_init_nullmodem(self, client, server): """Test init()""" @@ -205,7 +199,6 @@ async def test_is_active(self, client): assert client.is_active() client.transport_close() - @pytest.mark.parametrize("use_host", [NULLMODEM_HOST]) async def test_create_nullmodem(self, client, server): """Test create_nullmodem.""" assert not await client.transport_connect() @@ -253,26 +246,23 @@ def test_generate_ssl(self, use_clc): ) +@mock.patch( + "pymodbus.transport.transport_serial.serial.serial_for_url", mock.MagicMock() +) class TestBasicSerial: """Test transport serial module.""" @staticmethod @pytest.fixture(name="use_port") - def get_my_port(base_ports): + def get_port_in_class(base_ports): """Return next port""" base_ports[__class__.__name__] += 1 return base_ports[__class__.__name__] - @mock.patch( - "pymodbus.transport.transport_serial.serial.serial_for_url", mock.Mock() - ) async def test_init(self): """Test null modem init""" SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") - @mock.patch( - "pymodbus.transport.transport_serial.serial.serial_for_url", mock.Mock() - ) async def test_abstract_methods(self): """Test asyncio abstract methods.""" comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") @@ -289,9 +279,6 @@ async def test_abstract_methods(self): comm.resume_reading() comm.is_closing() - @mock.patch( - "pymodbus.transport.transport_serial.serial.serial_for_url", mock.Mock() - ) async def xtest_external_methods(self): """Test external methods.""" comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") diff --git a/test/sub_transport/test_comm.py b/test/sub_transport/test_comm.py index 1993fae958..f269f69acf 100644 --- a/test/sub_transport/test_comm.py +++ b/test/sub_transport/test_comm.py @@ -6,14 +6,12 @@ import pytest from pymodbus.transport import ( - NULLMODEM_HOST, CommType, ModbusProtocol, - NullModem, ) -FACTOR = 1.2 if not pytest.IS_WINDOWS else 3.2 +FACTOR = 1.2 if not pytest.IS_WINDOWS else 4.2 class TestCommModbusProtocol: @@ -21,21 +19,17 @@ class TestCommModbusProtocol: @staticmethod @pytest.fixture(name="use_port") - def get_my_port(base_ports): + def get_port_in_class(base_ports): """Return next port""" base_ports[__class__.__name__] += 1 return base_ports[__class__.__name__] - def teardown(self): - """Run class teardown""" - assert not NullModem.is_dirty() - @pytest.mark.parametrize( ("use_comm_type", "use_host"), [ (CommType.TCP, "localhost"), (CommType.TLS, "localhost"), - # (CommType.UDP, "localhost", BASE_PORT + 3), udp is connectionless. + # (CommType.UDP, "localhost"), udp is connectionless. (CommType.SERIAL, "socket://localhost:5004"), ], ) @@ -54,6 +48,7 @@ async def test_connect(self, client): (CommType.TLS, "illegal_host"), # (CommType.UDP, "illegal_host", BASE_PORT + 7), udp is connectionless. (CommType.SERIAL, "/dev/tty007pymodbus_5008"), + (CommType.SERIAL, "socket://illegal_host:5008"), ], ) async def test_connect_not_ok(self, client): @@ -188,12 +183,11 @@ class TestCommNullModem: @staticmethod @pytest.fixture(name="use_port") - def get_my_port(base_ports): + def get_port_in_class(base_ports): """Return next port""" - base_ports[__class__.__name__] += 1 + base_ports[__class__.__name__] += 2 return base_ports[__class__.__name__] - @pytest.mark.parametrize("use_host", [NULLMODEM_HOST]) async def test_single_connection(self, server, client): """Test single connection.""" await server.transport_listen() @@ -206,7 +200,6 @@ async def test_single_connection(self, server, client): client.transport_close() server.transport_close() - @pytest.mark.parametrize("use_host", [NULLMODEM_HOST]) async def test_single_flow(self, server, client): """Test single connection.""" await server.transport_listen() @@ -224,22 +217,23 @@ async def test_single_flow(self, server, client): connect.callback_data.assert_called_once() server.transport_close() - @pytest.mark.parametrize("use_host", [NULLMODEM_HOST]) - async def xtest_multi_connection(self, server, client): + async def test_multi_connection(self, server, client): """Test single connection.""" await server.transport_listen() await client.transport_connect() connect = list(server.active_connections.values())[0] new_params_server = server.comm_params.copy() - new_params_server.port += 1 + new_params_server.source_address = ( + new_params_server.source_address[0], + new_params_server.source_address[1] + 1, + ) new_params_client = client.comm_params.copy() - new_params_client.port = new_params_server.port + new_params_client.port = new_params_server.source_address[1] server2 = ModbusProtocol(new_params_server, True) client2 = ModbusProtocol(new_params_client, False) await server2.transport_listen() await client2.transport_connect() connect2 = list(server2.active_connections.values())[0] - assert connect.transport.protocol == connect assert connect.transport.other_modem == client.transport assert connect2.transport.protocol == connect2 @@ -248,17 +242,23 @@ async def xtest_multi_connection(self, server, client): assert client.transport.other_modem == connect.transport assert client2.transport.protocol == client2 assert client2.transport.other_modem == connect2.transport - client.transport_close() - client2.transport_close() + for obj in (client, client2, server, server2): + obj.transport_close() - @pytest.mark.parametrize("use_host", [NULLMODEM_HOST]) - async def xtest_triangle_flow(self, server, client): + async def test_triangle_flow(self, server, client): """Test single connection.""" await server.transport_listen() await client.transport_connect() connect = list(server.active_connections.values())[0] - server2 = ModbusProtocol(server.comm_params, True) - client2 = ModbusProtocol(client.comm_params, False) + new_params_server = server.comm_params.copy() + new_params_server.source_address = ( + new_params_server.source_address[0], + new_params_server.source_address[1] + 1, + ) + server2 = ModbusProtocol(new_params_server, True) + new_params_client = client.comm_params.copy() + new_params_client.port = new_params_server.source_address[1] + client2 = ModbusProtocol(new_params_client, False) await server2.transport_listen() await client2.transport_connect() connect2 = list(server2.active_connections.values())[0] @@ -282,3 +282,5 @@ async def xtest_triangle_flow(self, server, client): client2.callback_data.assert_called_once() connect.callback_data.assert_called_once() connect2.callback_data.assert_called_once() + for obj in (client, client2, server, server2): + obj.transport_close() diff --git a/test/sub_transport/test_nullmodem.py b/test/sub_transport/test_nullmodem.py index 3c9dac30f4..028d2bfded 100644 --- a/test/sub_transport/test_nullmodem.py +++ b/test/sub_transport/test_nullmodem.py @@ -11,40 +11,34 @@ class TestNullModem: @staticmethod @pytest.fixture(name="use_port") - def get_my_port(base_ports): + def get_port_in_class(base_ports): """Return next port""" base_ports[__class__.__name__] += 2 return base_ports[__class__.__name__] - def teardown(self): - """Run class teardown""" - assert not NullModem.is_dirty() - def test_init(self, dummy_protocol): """Test initialize.""" prot = dummy_protocol() NullModem(prot) prot.connection_made.assert_not_called() prot.connection_lost.assert_not_called() - assert True def test_close(self, dummy_protocol): """Test initialize.""" prot = dummy_protocol() - modem = NullModem(dummy_protocol()) + modem = NullModem(prot) modem.close() prot.connection_made.assert_not_called() - prot.connection_lost.assert_not_called() - assert True + prot.connection_lost.assert_called_once() + modem.close() # test _is_closing works. def test_listen(self, dummy_protocol, use_port): """Test listener (shared list)""" protocol = dummy_protocol(is_server=True) listen = NullModem.set_listener(use_port, protocol) assert NullModem.listeners[use_port] == protocol - if len(NullModem.listeners) > 1: - print("jan igen") assert len(NullModem.listeners) == 1 + assert not NullModem.connections listen.close() assert not NullModem.listeners protocol.connection_made.assert_not_called() @@ -52,62 +46,67 @@ def test_listen(self, dummy_protocol, use_port): def test_listen_twice(self, dummy_protocol, use_port): """Test exception when listening twice.""" - port1 = use_port - listen1 = NullModem.set_listener(port1, dummy_protocol(is_server=True)) + listen1 = NullModem.set_listener(use_port, dummy_protocol(is_server=True)) with pytest.raises(AssertionError): - NullModem.set_listener(port1, dummy_protocol(is_server=True)) + NullModem.set_listener(use_port, dummy_protocol(is_server=True)) listen1.close() - listen2 = NullModem.set_listener(port1, dummy_protocol(is_server=True)) + listen2 = NullModem.set_listener(use_port, dummy_protocol(is_server=True)) assert len(NullModem.listeners) == 1 listen2.close() def test_listen_triangle(self, dummy_protocol, use_port): """Test listener (shared list)""" - port1 = use_port - port2 = use_port + 1 - listen1 = NullModem.set_listener(port1, dummy_protocol(is_server=True)) - listen2 = NullModem.set_listener(port2, dummy_protocol(is_server=True)) + use_port2 = use_port + 1 + listen1 = NullModem.set_listener(use_port, dummy_protocol(is_server=True)) + listen2 = NullModem.set_listener(use_port2, dummy_protocol(is_server=True)) listen1.close() - assert port1 not in NullModem.listeners + assert use_port not in NullModem.listeners assert len(NullModem.listeners) == 1 listen2.close() assert not NullModem.listeners def test_connect(self, dummy_protocol, use_port): """Test connect.""" - port1 = use_port prot_listen = dummy_protocol(is_server=True) - listen1 = NullModem.set_listener(port1, prot_listen) + listen = NullModem.set_listener(use_port, prot_listen) prot1 = dummy_protocol() - modem1, _ = NullModem.set_connection(port1, prot1) - modem1b = modem1.other_modem - assert modem1.protocol != listen1.protocol - assert modem1.protocol != modem1b.protocol + modem, _ = NullModem.set_connection(use_port, prot1) + modem_b = modem.other_modem + assert modem.protocol != listen.protocol + assert modem.protocol != modem_b.protocol assert len(NullModem.connections) == 2 - assert NullModem.connections[modem1] == port1 - assert NullModem.connections[modem1b] == -port1 - modem1.close() - assert modem1b not in NullModem.connections - listen1.close() + assert NullModem.connections[modem] == use_port + assert NullModem.connections[modem_b] == -use_port + modem.close() + assert modem_b not in NullModem.connections + listen.close() prot_listen.connection_made.assert_not_called() prot_listen.connection_lost.assert_not_called() prot1.connection_made.assert_called_once() prot1.connection_lost.assert_called_once() - modem1b.protocol.connection_made.assert_called_once() - modem1b.protocol.connection_lost.assert_called_once() + modem_b.protocol.connection_made.assert_called_once() + modem_b.protocol.connection_lost.assert_called_once() def test_connect_no_listen(self, dummy_protocol, use_port): """Test connect without listen""" with pytest.raises(asyncio.TimeoutError): NullModem.set_connection(use_port, dummy_protocol()) + def test_listen_close(self, dummy_protocol, use_port): + """Test connect without listen""" + listen = NullModem.set_listener(use_port, dummy_protocol(is_server=True)) + modem, _ = NullModem.set_connection(use_port, dummy_protocol()) + listen.close() + assert len(NullModem.connections) == 2 + assert not NullModem.listeners + modem.close() + def test_connect_multiple(self, dummy_protocol, use_port): """Test multiple connect.""" - port1 = use_port - listen1 = NullModem.set_listener(port1, dummy_protocol(is_server=True)) - modem1, _ = NullModem.set_connection(port1, dummy_protocol()) + listen1 = NullModem.set_listener(use_port, dummy_protocol(is_server=True)) + modem1, _ = NullModem.set_connection(use_port, dummy_protocol()) modem1b = modem1.other_modem - modem2, _ = NullModem.set_connection(port1, dummy_protocol()) + modem2, _ = NullModem.set_connection(use_port, dummy_protocol()) modem2b = modem2.other_modem protocol_list = [ modem1.protocol, @@ -129,28 +128,36 @@ def test_connect_multiple(self, dummy_protocol, use_port): assert modem2b in NullModem.connections modem2.close() + def test_is_dirty(self, dummy_protocol, use_port): + """Test connect.""" + assert not NullModem.is_dirty() + listen = NullModem.set_listener(use_port, dummy_protocol(is_server=True)) + modem, _ = NullModem.set_connection(use_port, dummy_protocol()) + assert NullModem.is_dirty() + modem.close() + listen.close() + assert not NullModem.is_dirty() + def test_single_flow(self, dummy_protocol, use_port): """Test single flow.""" - port1 = use_port - listen1 = NullModem.set_listener(port1, dummy_protocol(is_server=True)) - modem1, _ = NullModem.set_connection(port1, dummy_protocol()) - modem1b = modem1.other_modem + listen = NullModem.set_listener(use_port, dummy_protocol(is_server=True)) + modem, _ = NullModem.set_connection(use_port, dummy_protocol()) + modem_b = modem.other_modem test_data1 = b"abcd" test_data2 = b"efgh" - modem1.sendto(test_data1) - modem1b.write(test_data2) - assert modem1b.protocol.data == test_data1 - assert modem1.protocol.data == test_data2 - modem1.close() - listen1.close() + modem.sendto(test_data1) + modem_b.write(test_data2) + assert modem_b.protocol.data == test_data1 + assert modem.protocol.data == test_data2 + modem.close() + listen.close() def test_triangle_flow(self, dummy_protocol, use_port): """Test triangle flow.""" - port1 = use_port - listen1 = NullModem.set_listener(port1, dummy_protocol(is_server=True)) - modem1, _ = NullModem.set_connection(port1, dummy_protocol()) + listen = NullModem.set_listener(use_port, dummy_protocol(is_server=True)) + modem1, _ = NullModem.set_connection(use_port, dummy_protocol()) modem1b = modem1.other_modem - modem2, _ = NullModem.set_connection(port1, dummy_protocol()) + modem2, _ = NullModem.set_connection(use_port, dummy_protocol()) modem2b = modem2.other_modem test_data1 = b"abcd" test_data2 = b"efgh" @@ -170,51 +177,41 @@ def test_triangle_flow(self, dummy_protocol, use_port): assert modem2.protocol.data == test_data4 modem1.close() modem2.close() - listen1.close() - - def test_manipulator_simple(self, dummy_protocol, use_port): - """Test manipulator.""" - add_text = b"MANIPULATED" - - def manipulator(data): - """Test manipulator""" - return [data + add_text] - - port1 = use_port - listen1 = NullModem.set_listener(port1, dummy_protocol(is_server=True)) - modem1, _ = NullModem.set_connection(port1, dummy_protocol()) - modem1b = modem1.other_modem - modem1.set_manipulator(manipulator) - test_data1 = b"abcd" - test_data2 = b"efgh" - modem1.write(test_data1) - modem1b.write(test_data2) - assert modem1b.protocol.data == test_data1 + add_text - assert modem1.protocol.data == test_data2 - modem1.close() - listen1.close() + listen.close() - def test_manipulator_adv(self, dummy_protocol, use_port): + @pytest.mark.parametrize( + "add_text", + [ + [b""], + [b"MANIPULATED"], + [b"MANIPULATED", b"and more"], + [b"MANIPULATED", b"and", b"much more"], + ], + ) + def test_manipulator(self, add_text, dummy_protocol, use_port): """Test manipulator.""" - add_text = b"MANIPULATED" def manipulator(data): """Test manipulator""" - return [data, add_text] - - port1 = use_port - listen1 = NullModem.set_listener(port1, dummy_protocol(is_server=True)) - modem1, _ = NullModem.set_connection(port1, dummy_protocol()) - modem1b = modem1.other_modem - modem1.set_manipulator(manipulator) - test_data1 = b"abcd" - test_data2 = b"efgh" - modem1.write(test_data1) - modem1b.write(test_data2) - assert modem1b.protocol.data == test_data1 + add_text - assert modem1.protocol.data == test_data2 - modem1.close() - listen1.close() + data = [data] + data.extend(add_text) + return data + + listen = NullModem.set_listener(use_port, dummy_protocol(is_server=True)) + modem, _ = NullModem.set_connection(use_port, dummy_protocol()) + modem_b = modem.other_modem + modem.set_manipulator(manipulator) + data1 = b"abcd" + data2 = b"efgh" + modem.write(data1) + modem.write(data2) + modem_b.write(data1) + modem_b.write(data2) + x = b"".join(part for part in add_text) + assert modem.protocol.data == data1 + data2 + assert modem_b.protocol.data == data1 + x + data2 + x + modem.close() + listen.close() async def test_serve_forever(self, dummy_protocol): """Test external methods.""" diff --git a/test/sub_transport/test_protocol.py b/test/sub_transport/test_protocol.py index f3383a8936..2bd4dfb69d 100644 --- a/test/sub_transport/test_protocol.py +++ b/test/sub_transport/test_protocol.py @@ -1,7 +1,7 @@ """Test transport.""" import pytest -from pymodbus.transport import CommType, NullModem +from pymodbus.transport import CommType COMM_TYPES = [ @@ -23,15 +23,11 @@ class TestModbusProtocol: @staticmethod @pytest.fixture(name="use_port") - def get_my_port(base_ports): + def get_port_in_class(base_ports): """Return next port""" base_ports[__class__.__name__] += 1 return base_ports[__class__.__name__] - def teardown(self): - """Run class teardown""" - assert not NullModem.is_dirty() - @pytest.mark.parametrize("use_comm_type", COMM_TYPES) async def test_init_client(self, client): """Test init()""" diff --git a/test/sub_transport/test_reconnect.py b/test/sub_transport/test_reconnect.py index d415aa6974..485ee3dbbf 100644 --- a/test/sub_transport/test_reconnect.py +++ b/test/sub_transport/test_reconnect.py @@ -4,77 +4,69 @@ import pytest -from pymodbus.transport import NullModem - class TestReconnectModbusProtocol: """Test transport module, base part.""" @staticmethod @pytest.fixture(name="use_port") - def get_my_port(base_ports): + def get_port_in_class(base_ports): """Return next port""" base_ports[__class__.__name__] += 1 return base_ports[__class__.__name__] - def teardown(self): - """Run class teardown""" - assert not NullModem.is_dirty() - async def test_no_reconnect_call(self, client): """Test connection_lost().""" client.loop = asyncio.get_running_loop() - client.loop.create_connection = mock.AsyncMock(return_value=(None, None)) + client.call_create = mock.AsyncMock(return_value=(None, None)) await client.transport_connect() client.connection_lost(RuntimeError("Connection lost")) assert not client.reconnect_task - assert client.loop.create_connection.call_count assert not client.reconnect_delay_current + assert client.call_create.called client.transport_close() - async def test_reconnect_call(self, client, use_clc): + async def test_reconnect_call(self, client): """Test connection_lost().""" client.loop = asyncio.get_running_loop() - client.loop.create_connection = mock.AsyncMock(return_value=(None, None)) + client.call_create = mock.AsyncMock(return_value=(None, None)) await client.transport_connect() client.connection_made(mock.Mock()) client.connection_lost(RuntimeError("Connection lost")) assert client.reconnect_task await asyncio.sleep(client.reconnect_delay_current * 1.8) assert client.reconnect_task - assert client.loop.create_connection.call_count == 2 - assert client.reconnect_delay_current == use_clc.reconnect_delay * 2 + assert client.call_create.call_count == 2 + assert client.reconnect_delay_current == client.comm_params.reconnect_delay * 2 client.transport_close() - async def test_multi_reconnect_call(self, client, use_clc): + async def test_multi_reconnect_call(self, client): """Test connection_lost().""" client.loop = asyncio.get_running_loop() - client.loop.create_connection = mock.AsyncMock(return_value=(None, None)) + client.call_create = mock.AsyncMock(return_value=(None, None)) await client.transport_connect() client.connection_made(mock.Mock()) client.connection_lost(RuntimeError("Connection lost")) await asyncio.sleep(client.reconnect_delay_current * 1.8) - assert client.loop.create_connection.call_count == 2 - assert client.reconnect_delay_current == use_clc.reconnect_delay * 2 + assert client.call_create.call_count == 2 + assert client.reconnect_delay_current == client.comm_params.reconnect_delay * 2 await asyncio.sleep(client.reconnect_delay_current * 1.8) - assert client.loop.create_connection.call_count == 3 - assert client.reconnect_delay_current == use_clc.reconnect_delay_max + assert client.call_create.call_count == 3 + assert client.reconnect_delay_current == client.comm_params.reconnect_delay_max await asyncio.sleep(client.reconnect_delay_current * 1.8) - assert client.loop.create_connection.call_count >= 4 - assert client.reconnect_delay_current == use_clc.reconnect_delay_max + assert client.call_create.call_count >= 4 + assert client.reconnect_delay_current == client.comm_params.reconnect_delay_max client.transport_close() - async def test_reconnect_call_ok(self, client, use_clc): + async def test_reconnect_call_ok(self, client): """Test connection_lost().""" client.loop = asyncio.get_running_loop() - client.loop.create_connection = mock.AsyncMock( - return_value=(mock.Mock(), mock.Mock()) - ) + client.call_create = mock.AsyncMock(return_value=(mock.Mock(), mock.Mock())) await client.transport_connect() client.connection_made(mock.Mock()) client.connection_lost(RuntimeError("Connection lost")) await asyncio.sleep(client.reconnect_delay_current * 1.8) - assert client.loop.create_connection.call_count == 2 - assert client.reconnect_delay_current == use_clc.reconnect_delay + assert client.call_create.call_count == 2 + assert client.reconnect_delay_current == client.comm_params.reconnect_delay assert not client.reconnect_task client.transport_close()