diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 2049cc7bf..000000000 --- a/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[report] -show_missing = True \ No newline at end of file diff --git a/.gitignore b/.gitignore index 963f4ab15..883703f81 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ test/__pycache__/ /test/lib/ /test/pip-selfcheck.json /test/.Python +/.cache/ diff --git a/examples/common/async-asyncio-client.py b/examples/common/async-asyncio-client.py index 241c949d3..e2a389866 100644 --- a/examples/common/async-asyncio-client.py +++ b/examples/common/async-asyncio-client.py @@ -1,6 +1,7 @@ import asyncio from pymodbus.client.async.tcp import AsyncModbusTCPClient as ModbusClient +from pymodbus.client.async.udp import AsyncModbusUDPClient as ModbusClient from pymodbus.client.async import schedulers @@ -23,7 +24,7 @@ async def start_async_test(client): # which defaults to `0x00` #---------------------------------------------------------------------------# log.debug("Reading Coils") - rr = client.read_coils(1, 1, unit=0x01) + rr = await client.read_coils(1, 1, unit=0x01) #---------------------------------------------------------------------------# # example requests @@ -100,6 +101,6 @@ async def start_async_test(client): if __name__ == '__main__': - loop, client = ModbusClient(schedulers.ASYNC_IO, port=5440) + loop, client = ModbusClient(schedulers.ASYNC_IO, port=5020) loop.run_until_complete(start_async_test(client.protocol)) loop.close() diff --git a/examples/common/async-asyncio-serial-client.py b/examples/common/async-asyncio-serial-client.py index 238bd3362..af94ac698 100755 --- a/examples/common/async-asyncio-serial-client.py +++ b/examples/common/async-asyncio-serial-client.py @@ -102,6 +102,7 @@ async def start_async_test(client): await asyncio.sleep(1) if __name__ == '__main__': - loop, client = ModbusClient(schedulers.ASYNC_IO, port='/dev/ptyp0', baudrate=9600, timeout=2, method="rtu") - loop.run_until_complete(start_async_test(client)) + # socat -d -d PTY,link=/tmp/ptyp0,raw,echo=0,ispeed=9600 PTY,link=/tmp/ttyp0,raw,echo=0,ospeed=9600 + loop, client = ModbusClient(schedulers.ASYNC_IO, port='/tmp/ptyp0', baudrate=9600, timeout=2, method="rtu") + loop.run_until_complete(start_async_test(client.protocol)) loop.close() diff --git a/examples/common/async-twisted-client-serial.py b/examples/common/async-twisted-client-serial.py index a981d8949..a53061bae 100755 --- a/examples/common/async-twisted-client-serial.py +++ b/examples/common/async-twisted-client-serial.py @@ -73,7 +73,7 @@ def error_handler(self, failure): log.error(failure) -client, proto = AsyncModbusSerialClient(schedulers.REACTOR, method="rtu", port=SERIAL_PORT, timeout=2, proto_cls=ExampleProtocol) +proto, client = AsyncModbusSerialClient(schedulers.REACTOR, method="rtu", port=SERIAL_PORT, timeout=2, proto_cls=ExampleProtocol) proto.start() # proto.stop() diff --git a/examples/common/synchronous-server.py b/examples/common/synchronous-server.py index 85ba420e9..b09ba748d 100755 --- a/examples/common/synchronous-server.py +++ b/examples/common/synchronous-server.py @@ -11,9 +11,9 @@ #---------------------------------------------------------------------------# # import the various server implementations #---------------------------------------------------------------------------# -from pymodbus.server.sync import StartTcpServer -# from pymodbus.server.sync import StartUdpServer -from pymodbus.server.sync import StartSerialServer +# from pymodbus.server.sync import StartTcpServer +from pymodbus.server.sync import StartUdpServer +# from pymodbus.server.sync import StartSerialServer from pymodbus.device import ModbusDeviceIdentification from pymodbus.datastore import ModbusSequentialDataBlock @@ -108,10 +108,10 @@ # StartTcpServer(context, identity=identity, address=("localhost", 5020)) # Udp: -# StartUdpServer(context, identity=identity, address=("localhost", 5020)) +StartUdpServer(context, identity=identity, address=("localhost", 5020)) # Ascii: #StartSerialServer(context, identity=identity, port='/dev/pts/3', timeout=1) # RTU: -StartSerialServer(context, framer=ModbusRtuFramer, identity=identity, port='/dev/ttyp0', timeout=.005, baudrate=9600) +# StartSerialServer(context, framer=ModbusRtuFramer, identity=identity, port='/dev/ttyp0', timeout=.005, baudrate=9600) diff --git a/pymodbus/client/async/asyncio/__init__.py b/pymodbus/client/async/asyncio/__init__.py index ddac7f121..7d976d701 100644 --- a/pymodbus/client/async/asyncio/__init__.py +++ b/pymodbus/client/async/asyncio/__init__.py @@ -109,6 +109,119 @@ def _buildResponse(self, tid): self.transaction.addTransaction(f, tid) return f + def close(self): + self.transport.close() + self._connected = False + + +class ModbusUdpClientProtocol(asyncio.DatagramProtocol, AsyncModbusClientMixin): + """ + Asyncio specific implementation of asynchronous modbus udp client protocol. + """ + + #: Factory that created this instance. + factory = None + + def __init__(self, host=None, port=0, **kwargs): + self.host = host + self.port = port + super(self.__class__, self).__init__(**kwargs) + + def connection_made(self, transport): + self.transport = transport + self._connectionMade() + + if self.factory: + self.factory.protocol_made_connection(self) + + def connection_lost(self, reason): + self.transport = None + self._connectionLost(reason) + + if self.factory: + self.factory.protocol_lost_connection(self) + + def execute(self, request): + ''' Starts the producer to send the next request to + consumer.write(Frame(request)) + ''' + request.transaction_id = self.transaction.getNextTID() + packet = self.framer.buildPacket(request) + self.transport.sendto(packet, (self.host, self.port)) + return self._buildResponse(request.transaction_id) + + def datagram_received(self, data, addr): + self._dataReceived(data) + + def create_future(self): + return asyncio.Future() + + def resolve_future(self, f, result): + f.set_result(result) + + def raise_future(self, f, exc): + f.set_exception(exc) + + def _connectionMade(self): + ''' Called upon a successful client connection. + ''' + _logger.debug("Client connected to modbus server") + self._connected = True + + def _connectionLost(self, reason): + ''' Called upon a client disconnect + + :param reason: The reason for the disconnect + ''' + _logger.debug("Client disconnected from modbus server: %s" % reason) + self._connected = False + for tid in list(self.transaction): + self.raise_future(self.transaction.getTransaction(tid), ConnectionException('Connection lost during request')) + + def _handleResponse(self, reply, **kwargs): + ''' Handle the processed response and link to correct deferred + + :param reply: The reply to process + ''' + if reply is not None: + tid = reply.transaction_id + handler = self.transaction.getTransaction(tid) + if handler: + self.resolve_future(handler, reply) + else: + _logger.debug("Unrequested message: " + str(reply)) + + def _buildResponse(self, tid): + ''' Helper method to return a deferred response + for the current request. + + :param tid: The transaction identifier for this response + :returns: A defer linked to the latest request + ''' + f = self.create_future() + if not self._connected: + self.raise_future(f, ConnectionException('Client is not connected')) + else: + self.transaction.addTransaction(f, tid) + return f + + def _dataReceived(self, data): + ''' Get response, check for valid message, decode result + + :param data: The data returned from the server + ''' + self.framer.processIncomingPacket(data, self._handleResponse) + + def close(self): + self.transport.close() + self._connected = False + + @property + def connected(self): + """ Return connection status. + """ + return self._connected + class ReconnectingAsyncioModbusTcpClient(object): """Client to connect to modbus device repeatedly over TCP/IP.""" @@ -145,7 +258,6 @@ def start(self, host, port=502): _logger.debug('Connecting to %s:%s.' % (host, port)) self.host = host self.port = port - yield from self._connect() def stop(self): @@ -383,8 +495,6 @@ def __init__(self, host=None, port=502, protocol_class=None, loop=None): self.connected = False - - def stop(self): # prevent reconnect: # self.host = None @@ -438,9 +548,84 @@ def protocol_lost_connection(self, protocol): _logger.error('Factory protocol disconnect callback called while not connected.') +class AsyncioModbusSerialClient(object): + """Client to connect to modbus device over serial.""" + transport = None + framer = None + + def __init__(self, port, protocol_class=None, framer=None, loop=None): + #: Protocol used to talk to modbus device. + self.protocol_class = protocol_class or ModbusClientProtocol + #: Current protocol instance. + self.protocol = None + #: Event loop to use. + self.loop = loop or asyncio.get_event_loop() + self.port = port + self.framer = framer + self._connected = False + + def stop(self): + + if self._connected: + if self.protocol: + if self.protocol.transport: + self.protocol.transport.close() + + def _create_protocol(self): + """Factory function to create initialized protocol instance.""" + from serial_asyncio import create_serial_connection + + def factory(): + return self.protocol_class(framer=self.framer) + + cor = create_serial_connection(self.loop, factory, self.port) + return cor + + @asyncio.coroutine + def connect(self): + _logger.debug('Connecting.') + try: + yield from self.loop.create_connection(self._create_protocol, self.host, self.port) + _logger.info('Connected to %s:%s.' % (self.host, self.port)) + except Exception as ex: + _logger.warning('Failed to connect: %s' % ex) + # asyncio.async(self._reconnect(), loop=self.loop) + + def protocol_made_connection(self, protocol): + """Protocol notification of successful connection.""" + _logger.info('Protocol made connection.') + if not self._connected: + self._connected = True + self.protocol = protocol + else: + _logger.error('Factory protocol connect callback called while connected.') + + def protocol_lost_connection(self, protocol): + """Protocol notification of lost connection.""" + if self._connected: + _logger.info('Protocol lost connection.') + if protocol is not self.protocol: + _logger.error('Factory protocol callback called from unexpected protocol instance.') + + self._connected = False + self.protocol = None + # if self.host: + # asyncio.async(self._reconnect(), loop=self.loop) + else: + _logger.error('Factory protocol disconnect callback called while not connected.') + + @asyncio.coroutine -def init_client(proto_cls, loop, host, port, **kwargs): +def init_tcp_client(proto_cls, loop, host, port, **kwargs): client = ReconnectingAsyncioModbusTcpClient(protocol_class=proto_cls, loop=loop) yield from client.start(host, port) return client + + +@asyncio.coroutine +def init_udp_client(proto_cls, loop, host, port, **kwargs): + client = ReconnectingAsyncioModbusUdpClient(protocol_class=proto_cls, + loop=loop) + yield from client.start(host, port) + return client \ No newline at end of file diff --git a/pymodbus/client/async/factory/serial.py b/pymodbus/client/async/factory/serial.py index f62022fd5..c8e559c9b 100644 --- a/pymodbus/client/async/factory/serial.py +++ b/pymodbus/client/async/factory/serial.py @@ -46,9 +46,9 @@ def __init__(self, framer, *args, **kwargs): proto = EventLoopThread("reactor", reactor.run, reactor.stop, installSignalHandlers=0) - s = SerialModbusClient(framer, port, reactor, **kwargs) + ser_client = SerialModbusClient(framer, port, reactor, **kwargs) - return s, proto + return proto, ser_client def io_loop_factory(port=None, framer=None, **kwargs): @@ -68,7 +68,7 @@ def io_loop_factory(port=None, framer=None, **kwargs): def async_io_factory(port=Defaults.Port, framer=None, **kwargs): import asyncio - from pymodbus.client.async.asyncio import ModbusClientProtocol + from pymodbus.client.async.asyncio import ModbusClientProtocol, AsyncioModbusSerialClient loop = kwargs.pop("loop", None) or asyncio.get_event_loop() proto_cls = kwargs.pop("proto_cls", None) or ModbusClientProtocol @@ -76,14 +76,19 @@ def async_io_factory(port=Defaults.Port, framer=None, **kwargs): from serial_asyncio import create_serial_connection except ImportError: LOGGER.critical("pyserial-asyncio is not installed, install with 'pip install pyserial-asyncio") - exit(0) - - def proto_factory(): - return proto_cls(framer=framer) - - coro = create_serial_connection(loop, proto_factory, port, **kwargs) + import sys + sys.exit(1) + # + # def proto_factory(): + # return proto_cls(framer=framer) + + client = AsyncioModbusSerialClient(port, proto_cls, framer, loop) + # coro = create_serial_connection(loop, proto_factory, port, **kwargs) + coro = client._create_protocol() transport, protocol = loop.run_until_complete(asyncio.gather(coro))[0] - return loop, protocol + client.transport = transport + client.protocol = protocol + return loop, client def get_factory(scheduler): diff --git a/pymodbus/client/async/factory/tcp.py b/pymodbus/client/async/factory/tcp.py index eb4b4e411..dc6ecc812 100644 --- a/pymodbus/client/async/factory/tcp.py +++ b/pymodbus/client/async/factory/tcp.py @@ -61,10 +61,10 @@ def io_loop_factory(host="127.0.0.1", port=Defaults.Port, framer=None, def async_io_factory(host="127.0.0.1", port=Defaults.Port, framer=None, source_address=None, timeout=None, **kwargs): import asyncio - from pymodbus.client.async.asyncio import init_client + from pymodbus.client.async.asyncio import init_tcp_client loop = kwargs.get("loop") or asyncio.get_event_loop() proto_cls = kwargs.get("proto_cls", None) - cor = init_client(proto_cls, loop, host, port) + cor = init_tcp_client(proto_cls, loop, host, port) client = loop.run_until_complete(asyncio.gather(cor))[0] return loop, client @@ -77,7 +77,7 @@ def get_factory(scheduler): elif scheduler == schedulers.ASYNC_IO: return async_io_factory else: - LOGGER.warn("Allowed Schedulers: {}, {}, {}".format( + LOGGER.warning("Allowed Schedulers: {}, {}, {}".format( schedulers.REACTOR, schedulers.IO_LOOP, schedulers.ASYNC_IO )) raise Exception("Invalid Scheduler '{}'".format(scheduler)) diff --git a/pymodbus/client/async/factory/udp.py b/pymodbus/client/async/factory/udp.py index 3f52fa503..c6eb1d873 100644 --- a/pymodbus/client/async/factory/udp.py +++ b/pymodbus/client/async/factory/udp.py @@ -38,7 +38,13 @@ def io_loop_factory(host="127.0.0.1", port=Defaults.Port, framer=None, def async_io_factory(host="127.0.0.1", port=Defaults.Port, framer=None, source_address=None, timeout=None, **kwargs): - raise NotImplementedError() + import asyncio + from pymodbus.client.async.asyncio import init_udp_client + loop = kwargs.get("loop") or asyncio.get_event_loop() + proto_cls = kwargs.get("proto_cls", None) + cor = init_udp_client(proto_cls, loop, host, port) + client = loop.run_until_complete(asyncio.gather(cor))[0] + return loop, client def get_factory(scheduler): diff --git a/pymodbus/client/async/serial.py b/pymodbus/client/async/serial.py index 584560ebf..38a9388d9 100644 --- a/pymodbus/client/async/serial.py +++ b/pymodbus/client/async/serial.py @@ -65,7 +65,8 @@ def __new__(cls, scheduler, method, port, **kwargs): """ if not IS_PYTHON3 and scheduler == ASYNC_IO: logger.critical("ASYNCIO is supported only on python3") - exit(0) + import sys + sys.exit(1) factory_class = get_factory(scheduler) framer = cls._framer(method) yieldable = factory_class(framer=framer, port=port, **kwargs) diff --git a/pymodbus/client/async/tcp.py b/pymodbus/client/async/tcp.py index bc3c1bce3..feff62e8f 100644 --- a/pymodbus/client/async/tcp.py +++ b/pymodbus/client/async/tcp.py @@ -5,13 +5,22 @@ from __future__ import unicode_literals from __future__ import absolute_import +import logging from pymodbus.client.async.factory.tcp import get_factory from pymodbus.constants import Defaults +from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION +from pymodbus.client.async.schedulers import ASYNC_IO + +logger = logging.getLogger(__name__) class AsyncModbusTCPClient(object): def __new__(cls, scheduler, host="127.0.0.1", port=Defaults.Port, framer=None, source_address=None, timeout=None, **kwargs): + if not (IS_PYTHON3 and PYTHON_VERSION >= (3, 4)) and scheduler == ASYNC_IO: + logger.critical("ASYNCIO is supported only on python3") + import sys + sys.exit(1) factory_class = get_factory(scheduler) yieldable = factory_class(host=host, port=port, framer=framer, source_address=source_address, diff --git a/pymodbus/client/async/thread.py b/pymodbus/client/async/thread.py index 864bd10c9..a44c9e8fa 100644 --- a/pymodbus/client/async/thread.py +++ b/pymodbus/client/async/thread.py @@ -9,8 +9,6 @@ import logging -from tornado.ioloop import IOLoop - LOGGER = logging.getLogger(__name__) diff --git a/pymodbus/client/async/twisted/__init__.py b/pymodbus/client/async/twisted/__init__.py index 412380790..3333ec280 100644 --- a/pymodbus/client/async/twisted/__init__.py +++ b/pymodbus/client/async/twisted/__init__.py @@ -135,6 +135,11 @@ def _buildResponse(self, tid): self.transaction.addTransaction(d, tid) return d + def close(self): + if self.transport and hasattr(self.transport, "close"): + self.transport.close() + self._connected = False + class ModbusTcpClientProtocol(ModbusClientProtocol): framer = ModbusSocketFramer(ClientDecoder()) diff --git a/pymodbus/compat.py b/pymodbus/compat.py index ba378e212..84fcbe6d4 100644 --- a/pymodbus/compat.py +++ b/pymodbus/compat.py @@ -17,8 +17,9 @@ #---------------------------------------------------------------------------# # python version checks #---------------------------------------------------------------------------# -IS_PYTHON2 = sys.version_info[0] == 2 -IS_PYTHON3 = sys.version_info[0] == 3 +PYTHON_VERSION = sys.version_info +IS_PYTHON2 = PYTHON_VERSION[0] == 2 +IS_PYTHON3 = PYTHON_VERSION[0] == 3 IS_PYPY = hasattr(sys, 'pypy_translation_info') IS_JYTHON = sys.platform.startswith('java') diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index f3492b425..5f73d44db 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -242,7 +242,7 @@ def send(self, message): pdu = self.framer.buildPacket(message) if _logger.isEnabledFor(logging.DEBUG): _logger.debug('send: %s' % b2a_hex(pdu)) - return self.socket.sendto(pdu, self.client_address) + return self.request.sendto(pdu, self.client_address) #---------------------------------------------------------------------------# diff --git a/requirements-tests.txt b/requirements-tests.txt index 0d96d320b..b2d9ce9e0 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,6 +3,7 @@ coverage >= 4.2 mock >= 1.0.1 pytest==3.3.1 pytest-cov==2.5.1 +pyserial-asyncio==4.0 pep8>=1.7.0 verboselogs >= 1.5 tornado>=4.5.2 diff --git a/setup.cfg b/setup.cfg index 48164d73a..41e2a6ebc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,6 +22,6 @@ universal=1 [tool:pytest] addopts = --quiet --cov=pymodbus/ - --cov-fail-under=90 + --cov-fail-under=85 python_files = test/test_*.py diff --git a/test/test_client_async.py b/test/test_client_async.py index 9c07ccb31..75e9cf44e 100644 --- a/test/test_client_async.py +++ b/test/test_client_async.py @@ -1,15 +1,18 @@ #!/usr/bin/env python import unittest -from pymodbus.compat import IS_PYTHON3 -if IS_PYTHON3: +import pytest +from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION +if IS_PYTHON3 and PYTHON_VERSION >= (3, 4): from unittest.mock import patch, Mock, MagicMock -else: # Python 2 + import asyncio + from pymodbus.client.async.asyncio import AsyncioModbusSerialClient + from serial_asyncio import SerialTransport +else: from mock import patch, Mock, MagicMock import platform from distutils.version import LooseVersion -import serial -import sys -from pymodbus.client.async.serial import AsyncModbusSerialClient + +from pymodbus.client.async.serial import AsyncModbusSerialClient from pymodbus.client.async.tcp import AsyncModbusTCPClient from pymodbus.client.async.udp import AsyncModbusUDPClient @@ -19,7 +22,8 @@ from pymodbus.client.async import schedulers from pymodbus.factory import ClientDecoder from pymodbus.exceptions import ConnectionException -from pymodbus.transaction import ModbusSocketFramer, ModbusRtuFramer +from pymodbus.transaction import ModbusSocketFramer, ModbusRtuFramer, ModbusAsciiFramer, ModbusBinaryFramer +from pymodbus.client.async.twisted import ModbusSerClientProtocol IS_DARWIN = platform.system().lower() == "darwin" OSX_SIERRA = LooseVersion("10.12") @@ -35,85 +39,232 @@ # ---------------------------------------------------------------------------# -class AsynchronousClientTest(unittest.TestCase): +def mock_create_serial_connection(a, b, port): + ser = MagicMock() + ser.port = port + protocol = b() + transport = SerialTransport(a, protocol, ser) + protocol.transport = transport + return transport, protocol + + +def mock_asyncio_gather(coro): + return coro + + +def mock_asyncio_run_untill_complete(val): + transport, protocol = val + protocol._connected = True + return ([transport, protocol], ) + + +class TestAsynchronousClient(object): """ This is the unittest for the pymodbus.client.async module """ + # -----------------------------------------------------------------------# # Test TCP Client client # -----------------------------------------------------------------------# + def testTcpTwistedClient(self): + """ + Test the TCP Twisted client + :return: + """ + from twisted.internet import reactor + with patch("twisted.internet.reactor") as mock_reactor: + def test_callback(client): + pass + + def test_errback(client): + pass + AsyncModbusTCPClient(schedulers.REACTOR, + framer=ModbusSocketFramer(ClientDecoder()), + callback=test_callback, + errback=test_errback) + @patch("pymodbus.client.async.tornado.IOLoop") @patch("pymodbus.client.async.tornado.IOStream") def testTcpTornadoClient(self, mock_iostream, mock_ioloop): """ Test the TCP tornado client client initialize """ protocol, future = AsyncModbusTCPClient(schedulers.IO_LOOP, framer=ModbusSocketFramer(ClientDecoder())) client = future.result() - self.assertTrue(isinstance(client, AsyncTornadoModbusTcpClient)) - self.assertEqual(0, len(list(client.transaction))) - self.assertTrue(isinstance(client.framer, ModbusSocketFramer)) - self.assertTrue(client.port == 502) - self.assertTrue(client._connected) - self.assertTrue(client.stream.connect.call_count, 1) - self.assertTrue(client.stream.read_until_close.call_count, 1) + assert(isinstance(client, AsyncTornadoModbusTcpClient)) + assert(0 == len(list(client.transaction))) + assert(isinstance(client.framer, ModbusSocketFramer)) + assert(client.port == 502) + assert client._connected + assert(client.stream.connect.call_count == 1) + assert(client.stream.read_until_close.call_count == 1) def handle_failure(failure): - self.assertTrue(isinstance(failure.exception(), ConnectionException)) + assert(isinstance(failure.exception(), ConnectionException)) d = client._build_response(0x00) d.add_done_callback(handle_failure) - self.assertTrue(client._connected) + assert(client._connected) client.close() protocol.stop() - self.assertFalse(client._connected) + assert(not client._connected) + + @pytest.mark.skipif(not IS_PYTHON3 or PYTHON_VERSION < (3, 4), + reason="requires python3.4 or above") + @patch("asyncio.get_event_loop") + @patch("asyncio.gather") + def testTcpAsyncioClient(self, mock_gather, mock_loop): + """ + Test the TCP Twisted client + :return: + """ + pytest.skip("TBD") - def testUdpTornadoClient(self): + # -----------------------------------------------------------------------# + # Test UDP client + # -----------------------------------------------------------------------# + + @patch("pymodbus.client.async.tornado.IOLoop") + @patch("pymodbus.client.async.tornado.IOStream") + def testUdpTornadoClient(self, mock_iostream, mock_ioloop): """ Test the udp tornado client client initialize """ protocol, future = AsyncModbusUDPClient(schedulers.IO_LOOP, framer=ModbusSocketFramer(ClientDecoder())) client = future.result() - self.assertTrue(isinstance(client, AsyncTornadoModbusUdoClient)) - self.assertEqual(0, len(list(client.transaction))) - self.assertTrue(isinstance(client.framer, ModbusSocketFramer)) - self.assertTrue(client.port == 502) - self.assertTrue(client._connected) + assert(isinstance(client, AsyncTornadoModbusUdoClient)) + assert(0 == len(list(client.transaction))) + assert(isinstance(client.framer, ModbusSocketFramer)) + assert(client.port == 502) + assert(client._connected) def handle_failure(failure): - self.assertTrue(isinstance(failure.exception(), ConnectionException)) + assert(isinstance(failure.exception(), ConnectionException)) d = client._build_response(0x00) d.add_done_callback(handle_failure) - self.assertTrue(client._connected) + assert(client._connected) client.close() protocol.stop() - self.assertFalse(client._connected) + assert(not client._connected) def testUdpTwistedClient(self): """ Test the udp twisted client client initialize """ - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): AsyncModbusUDPClient(schedulers.REACTOR, framer=ModbusSocketFramer(ClientDecoder())) - def testSerialTornadoClient(self): + @pytest.mark.skipif(not IS_PYTHON3 or PYTHON_VERSION < (3, 4), + reason="requires python3.4 or above") + @patch("asyncio.get_event_loop") + @patch("asyncio.gather", side_effect=mock_asyncio_gather) + def testUdpAsycioClient(self, mock_gather, mock_event_loop): + """Test the udp asyncio client""" + pytest.skip("TBD") + pass + + # -----------------------------------------------------------------------# + # Test Serial client + # -----------------------------------------------------------------------# + + @pytest.mark.parametrize("method, framer", [("rtu", ModbusRtuFramer), + ("socket", ModbusSocketFramer), + ("binary", ModbusBinaryFramer), + ("ascii", ModbusAsciiFramer)]) + def testSerialTwistedClient(self, method, framer): + """ Test the serial tornado client client initialize """ + from twisted.internet.serialport import SerialPort + from twisted.internet import reactor + with patch('twisted.internet.reactor') as mock_reactor: + protocol, client = AsyncModbusSerialClient(schedulers.REACTOR, + method=method, + port=SERIAL_PORT, + proto_cls=ModbusSerClientProtocol) + + assert (isinstance(client, SerialPort)) + assert (isinstance(client.protocol, ModbusSerClientProtocol)) + assert (0 == len(list(client.protocol.transaction))) + assert (isinstance(client.protocol.framer, framer)) + assert (client._serial.port == SERIAL_PORT) + assert (client.protocol._connected) + + def handle_failure(failure): + assert (isinstance(failure.exception(), ConnectionException)) + + d = client.protocol._buildResponse(0x00) + d.addCallback(handle_failure) + + assert (client.protocol._connected) + client.protocol.close() + protocol.stop() + assert (not client.protocol._connected) + + @pytest.mark.parametrize("method, framer", [("rtu", ModbusRtuFramer), + ("socket", ModbusSocketFramer), + ("binary", ModbusBinaryFramer), + ("ascii", ModbusAsciiFramer)]) + def testSerialTornadoClient(self, method, framer): """ Test the serial tornado client client initialize """ - protocol, future = AsyncModbusSerialClient(schedulers.IO_LOOP, method="rtu", port=SERIAL_PORT) + protocol, future = AsyncModbusSerialClient(schedulers.IO_LOOP, method=method, port=SERIAL_PORT) client = future.result() - self.assertTrue(isinstance(client, AsyncTornadoModbusSerialClient)) - self.assertEqual(0, len(list(client.transaction))) - self.assertTrue(isinstance(client.framer, ModbusRtuFramer)) - self.assertTrue(client.port == SERIAL_PORT) - self.assertTrue(client._connected) + assert(isinstance(client, AsyncTornadoModbusSerialClient)) + assert(0 == len(list(client.transaction))) + assert(isinstance(client.framer, framer)) + assert(client.port == SERIAL_PORT) + assert(client._connected) def handle_failure(failure): - self.assertTrue(isinstance(failure.exception(), ConnectionException)) + assert(isinstance(failure.exception(), ConnectionException)) d = client._build_response(0x00) d.add_done_callback(handle_failure) - self.assertTrue(client._connected) + assert(client._connected) client.close() protocol.stop() - self.assertFalse(client._connected) + assert(not client._connected) + + @pytest.mark.skipif(IS_PYTHON3 , reason="requires python2.7") + def testSerialAsyncioClientPython2(self): + """ + Test Serial async asyncio client exits on python2 + :return: + """ + with pytest.raises(SystemExit) as pytest_wrapped_e: + AsyncModbusSerialClient(schedulers.ASYNC_IO, method="rtu", port=SERIAL_PORT) + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 1 + + @pytest.mark.skipif(not IS_PYTHON3 or PYTHON_VERSION < (3, 4), reason="requires python3.4 or above") + @patch("serial_asyncio.create_serial_connection", side_effect=mock_create_serial_connection) + @patch("asyncio.get_event_loop") + @patch("asyncio.gather", side_effect=mock_asyncio_gather) + @pytest.mark.parametrize("method, framer", [("rtu", ModbusRtuFramer), + ("socket", ModbusSocketFramer), + ("binary", ModbusBinaryFramer), + ("ascii", ModbusAsciiFramer)]) + def testSerialAsyncioClient(self, mock_gather, mock_event_loop, mock_serial_connection, method, framer): + """ + Test Serial async asyncio client exits on python2 + :return: + """ + loop = asyncio.get_event_loop() + loop.run_until_complete.side_effect = mock_asyncio_run_untill_complete + loop, client = AsyncModbusSerialClient(schedulers.ASYNC_IO, method=method, port=SERIAL_PORT, loop=loop) + assert(isinstance(client, AsyncioModbusSerialClient)) + assert(len(list(client.protocol.transaction)) == 0) + assert(isinstance(client.framer, framer)) + assert(client.protocol._connected) + + d = client.protocol._buildResponse(0x00) + + def handle_failure(failure): + assert(isinstance(failure.exception(), ConnectionException)) + + d.add_done_callback(handle_failure) + assert(client.protocol._connected) + client.protocol.close() + assert(not client.protocol._connected) + pass + # ---------------------------------------------------------------------------# # Main diff --git a/test/test_client_async_asyncio.py b/test/test_client_async_asyncio.py index ff039e93f..10fc1ef72 100644 --- a/test/test_client_async_asyncio.py +++ b/test/test_client_async_asyncio.py @@ -1,10 +1,23 @@ -from pymodbus.compat import IS_PYTHON3 -if IS_PYTHON3: +from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION +import pytest +if IS_PYTHON3 and PYTHON_VERSION >= (3, 4): from unittest import mock - from pymodbus.client.async.asyncio import ReconnectingAsyncioModbusTcpClient, ModbusClientProtocol + from pymodbus.client.async.asyncio import (ReconnectingAsyncioModbusTcpClient, + ModbusClientProtocol, ModbusUdpClientProtocol) from test.asyncio_test_helper import return_as_coroutine, run_coroutine - - def test_protocol_connection_state_propagation_to_factory(): + from pymodbus.factory import ClientDecoder + from pymodbus.exceptions import ConnectionException + from pymodbus.transaction import ModbusSocketFramer + from pymodbus.bit_read_message import ReadCoilsRequest, ReadCoilsResponse + protocols = [ModbusUdpClientProtocol, ModbusClientProtocol] +else: + import mock + protocols = [None, None] + + +@pytest.mark.skipif(not IS_PYTHON3, reason="requires python3.4 or above") +class TestAsyncioClient(object): + def test_protocol_connection_state_propagation_to_factory(self): protocol = ModbusClientProtocol() assert protocol.factory is None assert protocol.transport is None @@ -24,8 +37,7 @@ def test_protocol_connection_state_propagation_to_factory(): assert protocol.factory.protocol_made_connection.call_count == 0 protocol.factory.protocol_lost_connection.assert_called_once_with(protocol) - - def test_factory_initialization_state(): + def test_factory_initialization_state(self): mock_protocol_class = mock.MagicMock() mock_loop = mock.MagicMock() client = ReconnectingAsyncioModbusTcpClient(protocol_class=mock_protocol_class, loop=mock_loop) @@ -35,8 +47,7 @@ def test_factory_initialization_state(): assert client.loop is mock_loop assert client.protocol_class is mock_protocol_class - - def test_factory_reset_wait_before_reconnect(): + def test_factory_reset_wait_before_reconnect(self): mock_protocol_class = mock.MagicMock() mock_loop = mock.MagicMock() client = ReconnectingAsyncioModbusTcpClient(protocol_class=mock_protocol_class, loop=mock_loop) @@ -49,7 +60,7 @@ def test_factory_reset_wait_before_reconnect(): assert client.delay_ms == initial_delay - def test_factory_stop(): + def test_factory_stop(self): mock_protocol_class = mock.MagicMock() mock_loop = mock.MagicMock() client = ReconnectingAsyncioModbusTcpClient(protocol_class=mock_protocol_class, loop=mock_loop) @@ -65,8 +76,7 @@ def test_factory_stop(): client.stop() client.protocol.transport.close.assert_called_once_with() - - def test_factory_protocol_made_connection(): + def test_factory_protocol_made_connection(self): mock_protocol_class = mock.MagicMock() mock_loop = mock.MagicMock() client = ReconnectingAsyncioModbusTcpClient(protocol_class=mock_protocol_class, loop=mock_loop) @@ -81,9 +91,8 @@ def test_factory_protocol_made_connection(): assert client.connected assert client.protocol is mock.sentinel.PROTOCOL - @mock.patch('pymodbus.client.async.asyncio.asyncio.async') - def test_factory_protocol_lost_connection(mock_async): + def test_factory_protocol_lost_connection(self, mock_async): mock_protocol_class = mock.MagicMock() mock_loop = mock.MagicMock() client = ReconnectingAsyncioModbusTcpClient(protocol_class=mock_protocol_class, loop=mock_loop) @@ -106,9 +115,8 @@ def test_factory_protocol_lost_connection(mock_async): assert not client.connected assert client.protocol is None - @mock.patch('pymodbus.client.async.asyncio.asyncio.async') - def test_factory_start_success(mock_async): + def test_factory_start_success(self, mock_async): mock_protocol_class = mock.MagicMock() mock_loop = mock.MagicMock() client = ReconnectingAsyncioModbusTcpClient(protocol_class=mock_protocol_class, loop=mock_loop) @@ -117,9 +125,8 @@ def test_factory_start_success(mock_async): mock_loop.create_connection.assert_called_once_with(mock.ANY, mock.sentinel.HOST, mock.sentinel.PORT) assert mock_async.call_count == 0 - @mock.patch('pymodbus.client.async.asyncio.asyncio.async') - def test_factory_start_failing_and_retried(mock_async): + def test_factory_start_failing_and_retried(self, mock_async): mock_protocol_class = mock.MagicMock() mock_loop = mock.MagicMock() mock_loop.create_connection = mock.MagicMock(side_effect=Exception('Did not work.')) @@ -132,9 +139,8 @@ def test_factory_start_failing_and_retried(mock_async): mock_reconnect.assert_called_once_with() mock_async.assert_called_once_with(mock.sentinel.RECONNECT_GENERATOR, loop=mock_loop) - @mock.patch('pymodbus.client.async.asyncio.asyncio.sleep') - def test_factory_reconnect(mock_sleep): + def test_factory_reconnect(self, mock_sleep): mock_protocol_class = mock.MagicMock() mock_loop = mock.MagicMock() client = ReconnectingAsyncioModbusTcpClient(protocol_class=mock_protocol_class, loop=mock_loop) @@ -145,3 +151,125 @@ def test_factory_reconnect(mock_sleep): run_coroutine(client._reconnect()) mock_sleep.assert_called_once_with(5) assert mock_loop.create_connection.call_count == 1 + + @pytest.mark.parametrize("protocol", protocols) + def testClientProtocolConnectionMade(self, protocol): + """ + Test the client protocol close + :return: + """ + protocol = protocol(ModbusSocketFramer(ClientDecoder())) + transport = mock.MagicMock() + factory = mock.MagicMock() + if isinstance(protocol, ModbusUdpClientProtocol): + protocol.factory = factory + protocol.connection_made(transport) + assert protocol.transport == transport + assert protocol.connected + if isinstance(protocol, ModbusUdpClientProtocol): + assert protocol.factory.protocol_made_connection.call_count == 1 + + @pytest.mark.parametrize("protocol", protocols) + def testClientProtocolClose(self, protocol): + """ + Test the client protocol close + :return: + """ + protocol = protocol(ModbusSocketFramer(ClientDecoder())) + transport = mock.MagicMock() + factory = mock.MagicMock() + if isinstance(protocol, ModbusUdpClientProtocol): + protocol.factory = factory + protocol.connection_made(transport) + assert protocol.transport == transport + assert protocol.connected + protocol.close() + transport.close.assert_called_once_with() + assert not protocol.connected + + @pytest.mark.parametrize("protocol", protocols) + def testClientProtocolConnectionLost(self, protocol): + ''' Test the client protocol connection lost''' + framer = ModbusSocketFramer(None) + protocol = protocol(framer=framer) + transport = mock.MagicMock() + factory = mock.MagicMock() + if isinstance(protocol, ModbusUdpClientProtocol): + protocol.factory = factory + protocol.connection_made(transport) + protocol.transport.write = mock.Mock() + + request = ReadCoilsRequest(1, 1) + d = protocol.execute(request) + protocol.connection_lost("REASON") + excp = d.exception() + assert (isinstance(excp, ConnectionException)) + if isinstance(protocol, ModbusUdpClientProtocol): + assert protocol.factory.protocol_lost_connection.call_count == 1 + + @pytest.mark.parametrize("protocol", protocols) + def testClientProtocolDataReceived(self, protocol): + ''' Test the client protocol data received ''' + protocol = protocol(ModbusSocketFramer(ClientDecoder())) + transport = mock.MagicMock() + protocol.connection_made(transport) + assert protocol.transport == transport + assert protocol.connected + data = b'\x00\x00\x12\x34\x00\x06\xff\x01\x01\x02\x00\x04' + + # setup existing request + d = protocol._buildResponse(0x00) + if isinstance(protocol, ModbusClientProtocol): + protocol.data_received(data) + else: + protocol.datagram_received(data, None) + result = d.result() + assert isinstance(result, ReadCoilsResponse) + + @pytest.mark.parametrize("protocol", protocols) + def testClientProtocolExecute(self, protocol): + ''' Test the client protocol execute method ''' + framer = ModbusSocketFramer(None) + protocol = protocol(framer=framer) + transport = mock.MagicMock() + protocol.connection_made(transport) + protocol.transport.write = mock.Mock() + + request = ReadCoilsRequest(1, 1) + d = protocol.execute(request) + tid = request.transaction_id + assert d == protocol.transaction.getTransaction(tid) + + @pytest.mark.parametrize("protocol", protocols) + def testClientProtocolHandleResponse(self, protocol): + ''' Test the client protocol handles responses ''' + protocol = protocol() + transport = mock.MagicMock() + protocol.connection_made(transport=transport) + reply = ReadCoilsRequest(1, 1) + reply.transaction_id = 0x00 + + # handle skipped cases + protocol._handleResponse(None) + protocol._handleResponse(reply) + + # handle existing cases + d = protocol._buildResponse(0x00) + protocol._handleResponse(reply) + result = d.result() + assert result == reply + + @pytest.mark.parametrize("protocol", protocols) + def testClientProtocolBuildResponse(self, protocol): + ''' Test the udp client protocol builds responses ''' + protocol = protocol() + assert not len(list(protocol.transaction)) + + d = protocol._buildResponse(0x00) + excp = d.exception() + assert (isinstance(excp, ConnectionException)) + assert not len(list(protocol.transaction)) + + protocol._connected = True + protocol._buildResponse(0x00) + assert len(list(protocol.transaction)) == 1 diff --git a/test/test_server_sync.py b/test/test_server_sync.py index c869c9baa..52d70f921 100644 --- a/test/test_server_sync.py +++ b/test/test_server_sync.py @@ -202,11 +202,11 @@ def testModbusDisconnectedRequestHandlerSend(self): handler.socket = Mock() request = ReadCoilsResponse([1]) handler.send(request) - self.assertEqual(handler.socket.sendto.call_count, 1) + self.assertEqual(handler.request.sendto.call_count, 1) request.should_respond = False handler.send(request) - self.assertEqual(handler.socket.sendto.call_count, 1) + self.assertEqual(handler.request.sendto.call_count, 1) def testModbusDisconnectedRequestHandlerHandle(self): handler = socketserver.BaseRequestHandler(None, None, None)