diff --git a/.gitignore b/.gitignore index 1a62fbcd8..984a4402e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ pymodbus.egg-info/ .vscode .idea .noseids - +*.db .idea/ .tox/ doc/api/epydoc/html/ diff --git a/examples/contrib/message_generator.py b/examples/contrib/message_generator.py index 47781c2f9..24c477c61 100755 --- a/examples/contrib/message_generator.py +++ b/examples/contrib/message_generator.py @@ -82,7 +82,7 @@ ReturnSlaveBusCharacterOverrunCountRequest, ReturnIopOverrunCountRequest, ClearOverrunCountRequest, - GetClearModbusPlusRequest, + GetClearModbusPlusRequest ] @@ -128,7 +128,7 @@ ReturnSlaveBusCharacterOverrunCountResponse, ReturnIopOverrunCountResponse, ClearOverrunCountResponse, - GetClearModbusPlusResponse, + GetClearModbusPlusResponse ] diff --git a/examples/contrib/message_parser.py b/examples/contrib/message_parser.py index 5d3110661..bd440fd55 100755 --- a/examples/contrib/message_parser.py +++ b/examples/contrib/message_parser.py @@ -146,6 +146,10 @@ def get_options(): help="If the incoming message is in hexadecimal format", action="store_true", dest="transaction", default=False) + parser.add_option("-t", "--transaction", + help="If the incoming message is in hexadecimal format", + action="store_true", dest="transaction", default=False) + (opt, arg) = parser.parse_args() if not opt.message and len(arg) > 0: diff --git a/pymodbus/pdu.py b/pymodbus/pdu.py index 70fb7911d..166f362c1 100644 --- a/pymodbus/pdu.py +++ b/pymodbus/pdu.py @@ -149,8 +149,8 @@ class ModbusExceptions(Singleton): @classmethod def decode(cls, code): ''' Given an error code, translate it to a - string error name. - + string error name. + :param code: The code number to translate ''' values = dict((v, k) for k, v in iteritems(cls.__dict__) diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index 05a05fff7..5dfdc1f65 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -161,7 +161,7 @@ def datagramReceived(self, data, addr): :param data: The data sent by the client ''' - _logger.debug("Client Connected [%s:%s]" % addr) + _logger.debug("Client Connected [%s]" % addr) if _logger.isEnabledFor(logging.DEBUG): _logger.debug(" ".join([hex(byte2int(x)) for x in data])) if not self.control.ListenOnly: diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 1b1b0d757..24756f19f 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -95,8 +95,7 @@ def _check_response(self, response): elif isinstance(self.client.framer, ModbusAsciiFramer): if len(response) >= 5 and int(response[3:5], 16) > 128: return False - elif isinstance(self.client.framer, (ModbusRtuFramer, - ModbusBinaryFramer)): + elif isinstance(self.client.framer, (ModbusRtuFramer, ModbusBinaryFramer)): if len(response) >= 2 and byte2int(response[1]) > 128: return False @@ -133,7 +132,6 @@ def execute(self, request): if not result and self.retry_on_empty: retries -= 1 continue - if _logger.isEnabledFor(logging.DEBUG): _logger.debug("recv: " + " ".join([hex(byte2int(x)) for x in result])) self.client.framer.processIncomingPacket(result, self.addTransaction) @@ -170,7 +168,7 @@ def _recv(self, expected_response_length): continue if isinstance(self.client.framer, ModbusSocketFramer): # Ommit UID, which is included in header size - h_size = self.client.framer._ModbusSocketFramer__hsize + h_size = self.client.framer._hsize length = struct.unpack(">H", result[4:6])[0] -1 expected_response_length = h_size + length @@ -373,9 +371,9 @@ def __init__(self, decoder): :param decoder: The decoder factory implementation to use ''' - self.__buffer = b'' - self.__header = {'tid':0, 'pid':0, 'len':0, 'uid':0} - self.__hsize = 0x07 + self._buffer = b'' + self._header = {'tid':0, 'pid':0, 'len':0, 'uid':0} + self._hsize = 0x07 self.decoder = decoder #-----------------------------------------------------------------------# @@ -385,16 +383,16 @@ def checkFrame(self): ''' Check and decode the next frame Return true if we were successful ''' - if len(self.__buffer) > self.__hsize: - self.__header['tid'], self.__header['pid'], \ - self.__header['len'], self.__header['uid'] = struct.unpack( - '>HHHB', self.__buffer[0:self.__hsize]) + if len(self._buffer) > self._hsize: + self._header['tid'], self._header['pid'], \ + self._header['len'], self._header['uid'] = struct.unpack( + '>HHHB', self._buffer[0:self._hsize]) # someone sent us an error? ignore it - if self.__header['len'] < 2: + if self._header['len'] < 2: self.advanceFrame() # we have at least a complete message, continue - elif len(self.__buffer) - self.__hsize + 1 >= self.__header['len']: + elif len(self._buffer) - self._hsize + 1 >= self._header['len']: return True # we don't have enough of a message yet, wait return False @@ -405,9 +403,9 @@ def advanceFrame(self): it or determined that it contains an error. It also has to reset the current frame header handle ''' - length = self.__hsize + self.__header['len'] - 1 - self.__buffer = self.__buffer[length:] - self.__header = {'tid':0, 'pid':0, 'len':0, 'uid':0} + length = self._hsize + self._header['len'] - 1 + self._buffer = self._buffer[length:] + self._header = {'tid':0, 'pid':0, 'len':0, 'uid':0} def isFrameReady(self): ''' Check if we should continue decode logic @@ -416,22 +414,22 @@ def isFrameReady(self): :returns: True if ready, False otherwise ''' - return len(self.__buffer) > self.__hsize + return len(self._buffer) > self._hsize def addToFrame(self, message): ''' Adds new packet data to the current frame buffer :param message: The most recent packet ''' - self.__buffer += message + self._buffer += message def getFrame(self): ''' Return the next frame from the buffered data :returns: The next full frame buffer ''' - length = self.__hsize + self.__header['len'] - 1 - return self.__buffer[self.__hsize:length] + length = self._hsize + self._header['len'] - 1 + return self._buffer[self._hsize:length] def populateResult(self, result): ''' @@ -440,9 +438,9 @@ def populateResult(self, result): :param result: The response packet ''' - result.transaction_id = self.__header['tid'] - result.protocol_id = self.__header['pid'] - result.unit_id = self.__header['uid'] + result.transaction_id = self._header['tid'] + result.protocol_id = self._header['pid'] + result.unit_id = self._header['uid'] #-----------------------------------------------------------------------# # Public Member Functions @@ -470,9 +468,9 @@ def processIncomingPacket(self, data, callback): self._process(callback) else: self.resetFrame() else: - if len(self.__buffer): + if len(self._buffer): # Possible error ??? - if self.__header['len'] < 2: + if self._header['len'] < 2: self._process(callback, error=True) break @@ -499,14 +497,14 @@ def resetFrame(self): end of the message (python just doesn't have the resolution to check for millisecond delays). ''' - self.__buffer = b'' - self.__header = {} + self._buffer = b'' + self._header = {} def getRawFrame(self): """ Returns the complete buffer """ - return self.__buffer + return self._buffer def buildPacket(self, message): ''' Creates a ready to send modbus packet @@ -565,11 +563,11 @@ def __init__(self, decoder): :param decoder: The decoder factory implementation to use ''' - self.__buffer = b'' - self.__header = {} - self.__hsize = 0x01 - self.__end = b'\x0d\x0a' - self.__min_frame_size = 4 + self._buffer = b'' + self._header = {} + self._hsize = 0x01 + self._end = b'\x0d\x0a' + self._min_frame_size = 4 self.decoder = decoder #-----------------------------------------------------------------------# @@ -582,9 +580,9 @@ def checkFrame(self): ''' try: self.populateHeader() - frame_size = self.__header['len'] - data = self.__buffer[:frame_size - 2] - crc = self.__buffer[frame_size - 2:frame_size] + frame_size = self._header['len'] + data = self._buffer[:frame_size - 2] + crc = self._buffer[frame_size - 2:frame_size] crc_val = (byte2int(crc[0]) << 8) + byte2int(crc[1]) return checkCRC(data, crc_val) except (IndexError, KeyError): @@ -597,11 +595,11 @@ def advanceFrame(self): current frame header handle ''' try: - self.__buffer = self.__buffer[self.__header['len']:] + self._buffer = self._buffer[self._header['len']:] except KeyError: # Error response, no header len found self.resetFrame() - self.__header = {} + self._header = {} def resetFrame(self): ''' Reset the entire message frame. @@ -611,8 +609,8 @@ def resetFrame(self): end of the message (python just doesn't have the resolution to check for millisecond delays). ''' - self.__buffer = b'' - self.__header = {} + self._buffer = b'' + self._header = {} def isFrameReady(self): ''' Check if we should continue decode logic @@ -621,24 +619,24 @@ def isFrameReady(self): :returns: True if ready, False otherwise ''' - return len(self.__buffer) > self.__hsize + return len(self._buffer) > self._hsize def populateHeader(self): ''' Try to set the headers `uid`, `len` and `crc`. - This method examines `self.__buffer` and writes meta - information into `self.__header`. It calculates only the + This method examines `self._buffer` and writes meta + information into `self._header`. It calculates only the values for headers that are not already in the dictionary. Beware that this method will raise an IndexError if - `self.__buffer` is not yet long enough. + `self._buffer` is not yet long enough. ''' - self.__header['uid'] = byte2int(self.__buffer[0]) - func_code = byte2int(self.__buffer[1]) + self._header['uid'] = byte2int(self._buffer[0]) + func_code = byte2int(self._buffer[1]) pdu_class = self.decoder.lookupPduClass(func_code) - size = pdu_class.calculateRtuFrameSize(self.__buffer) - self.__header['len'] = size - self.__header['crc'] = self.__buffer[size - 2:size] + size = pdu_class.calculateRtuFrameSize(self._buffer) + self._header['len'] = size + self._header['crc'] = self._buffer[size - 2:size] def addToFrame(self, message): ''' @@ -647,16 +645,16 @@ def addToFrame(self, message): :param message: The most recent packet ''' - self.__buffer += message + self._buffer += message def getFrame(self): ''' Get the next frame from the buffer :returns: The frame data or '' ''' - start = self.__hsize - end = self.__header['len'] - 2 - buffer = self.__buffer[start:end] + start = self._hsize + end = self._header['len'] - 2 + buffer = self._buffer[start:end] if end > 0: return buffer return '' @@ -668,7 +666,7 @@ def populateResult(self, result): :param result: The response packet ''' - result.unit_id = self.__header['uid'] + result.unit_id = self._header['uid'] #-----------------------------------------------------------------------# # Public Member Functions @@ -695,13 +693,13 @@ def processIncomingPacket(self, data, callback): self._process(callback) else: # Could be an error response - if len(self.__buffer): + if len(self._buffer): # Possible error ??? self._process(callback, error=True) else: - if len(self.__buffer): + if len(self._buffer): # Possible error ??? - if self.__header.get('len', 0) < 2: + if self._header.get('len', 0) < 2: self._process(callback, error=True) break @@ -736,7 +734,7 @@ def getRawFrame(self): """ Returns the complete buffer """ - return self.__buffer + return self._buffer @@ -764,11 +762,11 @@ def __init__(self, decoder): :param decoder: The decoder implementation to use ''' - self.__buffer = b'' - self.__header = {'lrc':'0000', 'len':0, 'uid':0x00} - self.__hsize = 0x02 - self.__start = b':' - self.__end = b"\r\n" + self._buffer = b'' + self._header = {'lrc':'0000', 'len':0, 'uid':0x00} + self._hsize = 0x02 + self._start = b':' + self._end = b"\r\n" self.decoder = decoder #-----------------------------------------------------------------------# @@ -779,19 +777,19 @@ def checkFrame(self): :returns: True if we successful, False otherwise ''' - start = self.__buffer.find(self.__start) + start = self._buffer.find(self._start) if start == -1: return False if start > 0 : # go ahead and skip old bad data - self.__buffer = self.__buffer[start:] + self._buffer = self._buffer[start:] start = 0 - end = self.__buffer.find(self.__end) + end = self._buffer.find(self._end) if (end != -1): - self.__header['len'] = end - self.__header['uid'] = int(self.__buffer[1:3], 16) - self.__header['lrc'] = int(self.__buffer[end - 2:end], 16) - data = a2b_hex(self.__buffer[start + 1:end - 2]) - return checkLRC(data, self.__header['lrc']) + self._header['len'] = end + self._header['uid'] = int(self._buffer[1:3], 16) + self._header['lrc'] = int(self._buffer[end - 2:end], 16) + data = a2b_hex(self._buffer[start + 1:end - 2]) + return checkLRC(data, self._header['lrc']) return False def advanceFrame(self): @@ -800,8 +798,8 @@ def advanceFrame(self): it or determined that it contains an error. It also has to reset the current frame header handle ''' - self.__buffer = self.__buffer[self.__header['len'] + 2:] - self.__header = {'lrc':'0000', 'len':0, 'uid':0x00} + self._buffer = self._buffer[self._header['len'] + 2:] + self._header = {'lrc':'0000', 'len':0, 'uid':0x00} def isFrameReady(self): ''' Check if we should continue decode logic @@ -810,7 +808,7 @@ def isFrameReady(self): :returns: True if ready, False otherwise ''' - return len(self.__buffer) > 1 + return len(self._buffer) > 1 def addToFrame(self, message): ''' Add the next message to the frame buffer @@ -819,16 +817,16 @@ def addToFrame(self, message): :param message: The most recent packet ''' - self.__buffer += message + self._buffer += message def getFrame(self): ''' Get the next frame from the buffer :returns: The frame data or '' ''' - start = self.__hsize + 1 - end = self.__header['len'] - 2 - buffer = self.__buffer[start:end] + start = self._hsize + 1 + end = self._header['len'] - 2 + buffer = self._buffer[start:end] if end > 0: return a2b_hex(buffer) return b'' @@ -840,8 +838,8 @@ def resetFrame(self): end of the message (python just doesn't have the resolution to check for millisecond delays). ''' - self.__buffer = b'' - self.__header = {'lrc':'0000', 'len':0, 'uid':0x00} + self._buffer = b'' + self._header = {'lrc':'0000', 'len':0, 'uid':0x00} def populateResult(self, result): ''' Populates the modbus result header @@ -851,7 +849,7 @@ def populateResult(self, result): :param result: The response packet ''' - result.unit_id = self.__header['uid'] + result.unit_id = self._header['uid'] #-----------------------------------------------------------------------# # Public Member Functions @@ -874,7 +872,8 @@ def processIncomingPacket(self, data, callback): self.addToFrame(data) while self.isFrameReady(): if self.checkFrame(): - result = self.decoder.decode(self.getFrame()) + frame = self.getFrame() + result = self.decoder.decode(frame) if result is None: raise ModbusIOException("Unable to decode response") self.populateResult(result) @@ -896,11 +895,11 @@ def buildPacket(self, message): packet = bytearray() params = (message.unit_id, message.function_code) - packet.extend(self.__start) + packet.extend(self._start) packet.extend(('%02x%02x' % params).encode()) packet.extend(b2a_hex(encoded)) packet.extend(('%02x' % checksum).encode()) - packet.extend(self.__end) + packet.extend(self._end) return bytes(packet).upper() @@ -937,12 +936,12 @@ def __init__(self, decoder): :param decoder: The decoder implementation to use ''' - self.__buffer = b'' - self.__header = {'crc':0x0000, 'len':0, 'uid':0x00} - self.__hsize = 0x01 - self.__start = b'\x7b' # { - self.__end = b'\x7d' # } - self.__repeat = [b'}'[0], b'{'[0]] # python3 hack + self._buffer = b'' + self._header = {'crc':0x0000, 'len':0, 'uid':0x00} + self._hsize = 0x01 + self._start = b'\x7b' # { + self._end = b'\x7d' # } + self._repeat = [b'}'[0], b'{'[0]] # python3 hack self.decoder = decoder #-----------------------------------------------------------------------# @@ -953,18 +952,18 @@ def checkFrame(self): :returns: True if we are successful, False otherwise ''' - start = self.__buffer.find(self.__start) + start = self._buffer.find(self._start) if start == -1: return False if start > 0 : # go ahead and skip old bad data - self.__buffer = self.__buffer[start:] + self._buffer = self._buffer[start:] - end = self.__buffer.find(self.__end) + end = self._buffer.find(self._end) if (end != -1): - self.__header['len'] = end - self.__header['uid'] = struct.unpack('>B', self.__buffer[1:2]) - self.__header['crc'] = struct.unpack('>H', self.__buffer[end - 2:end])[0] - data = self.__buffer[start + 1:end - 2] - return checkCRC(data, self.__header['crc']) + self._header['len'] = end + self._header['uid'] = struct.unpack('>B', self._buffer[1:2]) + self._header['crc'] = struct.unpack('>H', self._buffer[end - 2:end])[0] + data = self._buffer[start + 1:end - 2] + return checkCRC(data, self._header['crc']) return False def advanceFrame(self): @@ -973,8 +972,8 @@ def advanceFrame(self): it or determined that it contains an error. It also has to reset the current frame header handle ''' - self.__buffer = self.__buffer[self.__header['len'] + 2:] - self.__header = {'crc':0x0000, 'len':0, 'uid':0x00} + self._buffer = self._buffer[self._header['len'] + 2:] + self._header = {'crc':0x0000, 'len':0, 'uid':0x00} def isFrameReady(self): ''' Check if we should continue decode logic @@ -983,7 +982,7 @@ def isFrameReady(self): :returns: True if ready, False otherwise ''' - return len(self.__buffer) > 1 + return len(self._buffer) > 1 def addToFrame(self, message): ''' Add the next message to the frame buffer @@ -992,16 +991,16 @@ def addToFrame(self, message): :param message: The most recent packet ''' - self.__buffer += message + self._buffer += message def getFrame(self): ''' Get the next frame from the buffer :returns: The frame data or '' ''' - start = self.__hsize + 1 - end = self.__header['len'] - 2 - buffer = self.__buffer[start:end] + start = self._hsize + 1 + end = self._header['len'] - 2 + buffer = self._buffer[start:end] if end > 0: return buffer return b'' @@ -1013,7 +1012,7 @@ def populateResult(self, result): :param result: The response packet ''' - result.unit_id = self.__header['uid'] + result.unit_id = self._header['uid'] #-----------------------------------------------------------------------# # Public Member Functions @@ -1056,7 +1055,7 @@ def buildPacket(self, message): message.unit_id, message.function_code) + data packet += struct.pack(">H", computeCRC(packet)) - packet = self.__start + packet + self.__end + packet = self._start + packet + self._end return packet def _preflight(self, data): @@ -1070,7 +1069,7 @@ def _preflight(self, data): ''' array = bytearray() for d in data: - if d in self.__repeat: + if d in self._repeat: array.append(d) array.append(d) return bytes(array) @@ -1083,8 +1082,8 @@ def resetFrame(self): end of the message (python just doesn't have the resolution to check for millisecond delays). ''' - self.__buffer = b'' - self.__header = {'crc': 0x0000, 'len': 0, 'uid': 0x00} + self._buffer = b'' + self._header = {'crc': 0x0000, 'len': 0, 'uid': 0x00} #---------------------------------------------------------------------------# # Exported symbols diff --git a/test/test_server_async.py b/test/test_server_async.py index 89344f73c..8fbd9758f 100644 --- a/test/test_server_async.py +++ b/test/test_server_async.py @@ -2,14 +2,19 @@ from pymodbus.compat import IS_PYTHON3 import unittest if IS_PYTHON3: # Python 3 - from unittest.mock import patch, Mock + from unittest.mock import patch, Mock, MagicMock else: # Python 2 - from mock import patch, Mock + from mock import patch, Mock, MagicMock from pymodbus.device import ModbusDeviceIdentification from pymodbus.server.async import ModbusTcpProtocol, ModbusUdpProtocol from pymodbus.server.async import ModbusServerFactory -from pymodbus.server.async import StartTcpServer, StartUdpServer, StartSerialServer - +from pymodbus.server.async import ( + StartTcpServer, StartUdpServer, StartSerialServer, StopServer, + _is_main_thread +) +from pymodbus.compat import byte2int +from pymodbus.transaction import ModbusSocketFramer +from pymodbus.exceptions import NoSuchSlaveException, ModbusIOException import sys #---------------------------------------------------------------------------# @@ -28,7 +33,6 @@ class AsynchronousServerTest(unittest.TestCase): #-----------------------------------------------------------------------# # Setup/TearDown #-----------------------------------------------------------------------# - def setUp(self): ''' Initializes the test environment @@ -41,9 +45,112 @@ def tearDown(self): pass #-----------------------------------------------------------------------# - # Test Modbus Server Factory + # Test ModbusTcpProtocol #-----------------------------------------------------------------------# + def testTcpServerStartup(self): + ''' Test that the modbus tcp async server starts correctly ''' + with patch('twisted.internet.reactor') as mock_reactor: + if IS_PYTHON3: + console = False + call_count = 1 + else: + console = True + call_count = 2 + StartTcpServer(context=None, console=console) + self.assertEqual(mock_reactor.listenTCP.call_count, call_count) + self.assertEqual(mock_reactor.run.call_count, 1) + + def testConnectionMade(self): + protocol = ModbusTcpProtocol() + protocol.transport = MagicMock() + protocol.factory = MagicMock() + protocol.factory.framer = ModbusSocketFramer + protocol.connectionMade() + self.assertIsInstance(protocol.framer, ModbusSocketFramer) + + def testConnectionLost(self): + protocol = ModbusTcpProtocol() + protocol.connectionLost("What ever reason") + + def testDataReceived(self): + protocol = ModbusTcpProtocol() + # mock_data = "Hellow world!" + mock_data = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12\x34" + protocol.factory = MagicMock() + protocol.factory.control.ListenOnly = False + protocol.factory.store = [byte2int(mock_data[0])] + protocol.framer = protocol._execute = MagicMock() + + protocol.dataReceived(mock_data) + self.assertTrue(protocol.framer.processIncomingPacket.called) + # test datareceived returns None + protocol.factory.control.ListenOnly = False + self.assertEqual(protocol.dataReceived(mock_data), None) + + def testTcpExecuteSuccess(self): + protocol = ModbusTcpProtocol() + protocol.store = MagicMock() + request = MagicMock() + protocol._send = MagicMock() + + # tst if _send being called + protocol._execute(request) + self.assertTrue(protocol._send.called) + + def testTcpExecuteFailure(self): + protocol = ModbusTcpProtocol() + protocol.store = MagicMock() + request = MagicMock() + protocol._send = MagicMock() + + # CASE-1: test NoSuchSlaveException exceptions + request.execute.side_effect = NoSuchSlaveException() + self.assertRaises( + NoSuchSlaveException, protocol._execute(request) + ) + self.assertTrue(request.doException.called) + + # CASE-2: NoSuchSlaveException with ignore_missing_slaves = true + protocol.ignore_missing_slaves = True + request.execute.side_effect = NoSuchSlaveException() + self.assertEqual(protocol._execute(request), None) + + # test other exceptions + request.execute.side_effect = ModbusIOException() + self.assertRaises( + ModbusIOException, protocol._execute(request) + ) + self.assertTrue(protocol._send.called) + + def testSendTcp(self): + + class MockMsg(object): + def __init__(self, msg, resp=False): + self.should_respond = resp + self.msg = msg + + mock_msg = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12\x34" + protocol = ModbusTcpProtocol() + mock_data = MockMsg(resp=True, msg=mock_msg) + + protocol.control = MagicMock() + protocol.framer = MagicMock() + protocol.factory = MagicMock() + protocol.framer.buildPacket = MagicMock(return_value=mock_msg) + protocol.transport= MagicMock() + + protocol._send(mock_data) + + self.assertTrue(protocol.framer.buildPacket.called) + self.assertTrue(protocol.transport.write.called) + + mock_data =MockMsg(resp=False, msg="helloworld") + self.assertEqual(protocol._send(mock_data), None) + + #-----------------------------------------------------------------------# + # Test ModbusServerFactory + #-----------------------------------------------------------------------# def testModbusServerFactory(self): ''' Test the base class for all the clients ''' factory = ModbusServerFactory(store=None) @@ -54,14 +161,7 @@ def testModbusServerFactory(self): self.assertEqual(factory.control.Identity.VendorName, 'VendorName') #-----------------------------------------------------------------------# - # Test Modbus TCP Server - #-----------------------------------------------------------------------# - def testTCPServerDisconnect(self): - protocol = ModbusTcpProtocol() - protocol.connectionLost('because of an error') - - #-----------------------------------------------------------------------# - # Test Modbus UDP Server + # Test ModbusUdpProtocol #-----------------------------------------------------------------------# def testUdpServerInitialize(self): protocol = ModbusUdpProtocol(store=None) @@ -71,22 +171,6 @@ def testUdpServerInitialize(self): protocol = ModbusUdpProtocol(store=None, identity=identity) self.assertEqual(protocol.control.Identity.VendorName, 'VendorName') - #-----------------------------------------------------------------------# - # Test Modbus Server Startups - #-----------------------------------------------------------------------# - - def testTcpServerStartup(self): - ''' Test that the modbus tcp async server starts correctly ''' - with patch('twisted.internet.reactor') as mock_reactor: - if IS_PYTHON3: - console = False - call_count = 1 - else: - console = True - call_count = 2 - StartTcpServer(context=None, console=console) - self.assertEqual(mock_reactor.listenTCP.call_count, call_count) - self.assertEqual(mock_reactor.run.call_count, 1) def testUdpServerStartup(self): ''' Test that the modbus udp async server starts correctly ''' @@ -101,6 +185,83 @@ def testSerialServerStartup(self): StartSerialServer(context=None, port=SERIAL_PORT) self.assertEqual(mock_reactor.run.call_count, 1) + def testDatagramReceived(self): + mock_data = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12\x34" + mock_addr = 0x01 + protocol = ModbusUdpProtocol(store=None) + protocol.framer.processIncomingPacket = MagicMock() + protocol.control.ListenOnly = False + protocol._execute = MagicMock() + + protocol.datagramReceived(mock_data, mock_addr) + self.assertTrue(protocol.framer.processIncomingPacket.called) + + def testSendUdp(self): + protocol = ModbusUdpProtocol(store=None) + mock_data = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12\x34" + mock_addr = 0x01 + + protocol.control = MagicMock() + protocol.framer = MagicMock() + protocol.framer.buildPacket = MagicMock(return_value=mock_data) + protocol.transport= MagicMock() + + protocol._send(mock_data, mock_addr) + + self.assertTrue(protocol.framer.buildPacket.called) + self.assertTrue(protocol.transport.write.called) + + + def testUdpExecuteSuccess(self): + protocol = ModbusUdpProtocol(store=None) + mock_addr = 0x01 + protocol.store = MagicMock() + request = MagicMock() + protocol._send = MagicMock() + + # tst if _send being called + protocol._execute(request, mock_addr) + self.assertTrue(protocol._send.called) + + def testUdpExecuteFailure(self): + protocol = ModbusUdpProtocol(store=None) + mock_addr = 0x01 + protocol.store = MagicMock() + request = MagicMock() + protocol._send = MagicMock() + + # CASE-1: test NoSuchSlaveException exceptions + request.execute.side_effect = NoSuchSlaveException() + self.assertRaises( + NoSuchSlaveException, protocol._execute(request, mock_addr) + ) + self.assertTrue(request.doException.called) + + # CASE-2: NoSuchSlaveException with ignore_missing_slaves = true + protocol.ignore_missing_slaves = True + request.execute.side_effect = NoSuchSlaveException() + self.assertEqual(protocol._execute(request, mock_addr), None) + + # test other exceptions + request.execute.side_effect = ModbusIOException() + self.assertRaises( + ModbusIOException, protocol._execute(request, mock_addr) + ) + self.assertTrue(protocol._send.called) + + def testStopServer(self): + from twisted.internet import reactor + reactor.stop = MagicMock() + StopServer() + + self.assertTrue(reactor.stop.called) + + def testIsMainThread(self): + import threading + self.assertTrue(_is_main_thread()) + + + #---------------------------------------------------------------------------# # Main #---------------------------------------------------------------------------# diff --git a/test/test_transaction.py b/test/test_transaction.py index 8c5e18f47..2627bd380 100644 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -3,8 +3,16 @@ from binascii import a2b_hex from pymodbus.pdu import * from pymodbus.transaction import * +from pymodbus.transaction import ( + ModbusTransactionManager, ModbusSocketFramer, ModbusAsciiFramer, + ModbusRtuFramer, ModbusBinaryFramer +) from pymodbus.factory import ServerDecoder from pymodbus.compat import byte2int +from mock import MagicMock +from pymodbus.exceptions import ( + NotImplementedException, ModbusIOException, InvalidMessageRecievedException +) class ModbusTransactionTest(unittest.TestCase): ''' @@ -24,6 +32,7 @@ def setUp(self): self._binary = ModbusBinaryFramer(decoder=self.decoder) self._manager = DictTransactionManager(self.client) self._queue_manager = FifoTransactionManager(self.client) + self._tm = ModbusTransactionManager(self.client) def tearDown(self): ''' Cleans up the test environment ''' @@ -175,7 +184,7 @@ def testTCPFramerTransactionShort(self): self.assertEqual(b'', result) self._tcp.advanceFrame() self._tcp.addToFrame(msg2) - self.assertEqual(10, len(self._tcp._ModbusSocketFramer__buffer)) + self.assertEqual(10, len(self._tcp._buffer)) self.assertTrue(self._tcp.checkFrame()) result = self._tcp.getFrame() self.assertEqual(msg2[7:], result) @@ -257,7 +266,7 @@ def testRTUFramerPopulate(self): self._rtu.populateHeader() self._rtu.populateResult(request) - header_dict = self._rtu._ModbusRtuFramer__header + header_dict = self._rtu._header self.assertEqual(len(msg), header_dict['len']) self.assertEqual(byte2int(msg[0]), header_dict['uid']) self.assertEqual(msg[-2:], header_dict['crc']) @@ -283,6 +292,43 @@ def testRTUDecodeException(self): result = self._rtu.checkFrame() self.assertTrue(result) + def testProcess(self): + class MockResult(object): + def __init__(self, code): + self.function_code = code + + def mock_callback(self): + pass + + mock_result = MockResult(code=0) + self._rtu.getRawFrame = self._rtu.getFrame = MagicMock() + self._rtu.decoder = MagicMock() + self._rtu.decoder.decode = MagicMock(return_value=mock_result) + self._rtu.populateResult = MagicMock() + self._rtu.advanceFrame = MagicMock() + + self._rtu._process(mock_callback) + self._rtu.populateResult.assert_called_with(mock_result) + self._rtu.advanceFrame.assert_called_with() + self.assertTrue(self._rtu.advanceFrame.called) + + #Check errors + self._rtu.decoder.decode = MagicMock(return_value=None) + self.assertRaises(ModbusIOException, lambda: self._rtu._process(mock_callback)) + + def testRTUProcessIncomingPAkcets(self): + mock_data = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" + + def mock_callback(self): + pass + + self._rtu.addToFrame = MagicMock() + self._rtu._process = MagicMock() + self._rtu.isFrameReady = MagicMock(return_value=False) + self._rtu._buffer = mock_data + + self._rtu.processIncomingPacket(mock_data, mock_callback) + #---------------------------------------------------------------------------# # ASCII tests #---------------------------------------------------------------------------# @@ -342,6 +388,19 @@ def testASCIIFramerPacket(self): self.assertEqual(expected, actual) ModbusRequest.encode = old_encode + def testAsciiProcessIncomingPakcets(self): + mock_data = msg = b':F7031389000A60\r\n' + + def mock_callback(mock_data): + pass + + self._ascii.processIncomingPacket(mock_data, mock_callback) + + # Test failure: + self._ascii.checkFrame = MagicMock(return_value=False) + self._ascii.processIncomingPacket(mock_data, mock_callback) + + #---------------------------------------------------------------------------# # Binary tests #---------------------------------------------------------------------------# @@ -401,6 +460,18 @@ def testBinaryFramerPacket(self): self.assertEqual(expected, actual) ModbusRequest.encode = old_encode + def testBinaryProcessIncomingPacket(self): + mock_data = b'\x7b\x01\x03\x00\x00\x00\x05\x85\xC9\x7d' + + def mock_callback(mock_data): + pass + + self._binary.processIncomingPacket(mock_data, mock_callback) + + # Test failure: + self._binary.checkFrame = MagicMock(return_value=False) + self._binary.processIncomingPacket(mock_data, mock_callback) + #---------------------------------------------------------------------------# # Main #---------------------------------------------------------------------------#