From 829cddc849a096dd1f0e03ec9c58617c6432f82b Mon Sep 17 00:00:00 2001 From: dhoomakethu Date: Tue, 28 Nov 2017 12:11:02 +0530 Subject: [PATCH 01/15] Test dev (#240) * PYM-2: - The issue listed was due to wrong messages being passed by the user. Upon passing the right messages codes, the parser works as expected on all counts. - There is also changes that make the parser compatible with python2 and python3 - Verifier that the tool works on both python3 and python2 for all MODBUS message codes on TCP, RTU, and BIN * PYM-2: Checking the incoming framer. If te Framer is a Binary Framer, we take the unit address as the second incoming bite as opposed to the first bite. This is was done while fixing the message parsers for binary messages * PYM-2: Changed the modbus binary header size from 2 to 1. According to the docs: Modbus Binary Frame Controller:: [ Start ][Address ][ Function ][ Data ][ CRC ][ End ] 1b 1b 1b Nb 2b 1b * PYM-3: Script is now compatible with both python2 and python3 * WIP * PYM-2: Added a new switch: -t --transaction This switch is meant to be used when we wish to parse messages directly from the logs of Modbus. The format of a message as shown in the logs is like bellow: 0x7b 0x1 0x5 0x0 0x0 0xff 0x0 0x8c 0x3a 0x7d We can pass this as the message to the parser along with the -t witch to convert it into a compatible message to be parsed. EG: (modbus3) [~/pymodbus/examples/contrib]$ ./message-parser.py -b -t -p binary -m "0x7b 0x1 0x5 0x0 0x0 0xff 0x0 0x8c 0x3a 0x7d" ================================================================================ Decoding Message b'7b01050000ff008c3a7d' ================================================================================ ServerDecoder -------------------------------------------------------------------------------- name = WriteSingleCoilRequest transaction_id = 0x0 protocol_id = 0x0 unit_id = . [1] skip_encode = 0x0 check = 0x0 address = 0x0 value = 0x1 documentation = This function code is used to write a single output to either ON or OFF in a remote device. The requested ON/OFF state is specified by a constant in the request data field. A value of FF 00 hex requests the output to be ON. A value of 00 00 requests it to be OFF. All other values are illegal and will not affect the output. The Request PDU specifies the address of the coil to be forced. Coils are addressed starting at zero. Therefore coil numbered 1 is addressed as 0. The requested ON/OFF state is specified by a constant in the Coil Value field. A value of 0XFF00 requests the coil to be ON. A value of 0X0000 requests the coil to be off. All other values are illegal and will not affect the coil. ClientDecoder -------------------------------------------------------------------------------- name = WriteSingleCoilResponse transaction_id = 0x0 protocol_id = 0x0 unit_id = . [1] skip_encode = 0x0 check = 0x0 address = 0x0 value = 0x1 documentation = The normal response is an echo of the request, returned after the coil state has been written. * PYM-2: Removing additional dependancy and making use of existing porting tools * PYM-3: Removing additional dependancy and making use of existing porting tools * Initial Bitbucket Pipelines configuration * bitbucket-pipelines.yml edited online with Bitbucket * bitbucket-pipelines.yml edited online with Bitbucket * PYM-2: Updated the transaction tests for BinaryFramerTransaction. The header for Binary trasaction is of size 1. This was recrtified earlier commits of this branch. The test ensure these changes * PYM-6: Minor Cleanup task Removing the argument handler in TCP Syncronous server. This argument is not used any where. * PYM-6: ModbusUdpServer and ModbusTcpServer will now accept any legal Modbus request handler. The request handler being passed will have to be of instance ModbusBaseRequestHandler. The default request handler is ModbusDisconnectedRequestHandler. I.e., is no handler is passed, or if the handler is not of type ModbusBaseRequestHandler, ModbusDisconnectedRequestHandler will be made use of. * PYM-6: Removing uneccessary check if handler is of type ModbusBaseRequestHandler * PYM-8: Example that read from a database as a datastore * PYM-8: Added two new datastores that can be used. - SQLite3 - Reddis * Small fixes * Small fixes * Small fixes * Cleanup * PYM-8: Updated the example to first write a random value at a random afddress to a database and then read from that address * PYM-8: Added neccessary checks and methods to allow hassle free writes to database. The process first checks if the address and value are already present in the database before performing a write. This ensures that database transaction errors will now occur in cases where repetetive data is being written. * Cleanup: Removing pdb placed during testing and other comments * bitbucket-pipelines.yml deleted online with Bitbucket * #240 Fix PR failures * #240 fix Travis build failures * #190 fix import error in dbstore-update-server example * Small changes and typo fixed in pymodbus utilities and redis datastore helpers * Added tests for redis datastore helpers * Minor fixes to SQL datastore * Unit tests for SQL datastore - 100% coverage * Tests now compatible with python3 and python2 --- examples/common/dbstore-update-server.py | 94 +++++++ examples/contrib/message-generator.py | 27 +- examples/contrib/message-parser.py | 40 ++- pymodbus/datastore/database/__init__.py | 7 + .../datastore/database/redis_datastore.py | 104 ++++---- .../datastore/database/sql_datastore.py | 53 ++-- pymodbus/server/sync.py | 11 +- pymodbus/transaction.py | 6 +- pymodbus/utilities.py | 9 +- requirements-tests.txt | 3 + test/test_datastore.py | 238 +++++++++++++++++- test/test_transaction.py | 14 +- 12 files changed, 494 insertions(+), 112 deletions(-) create mode 100644 examples/common/dbstore-update-server.py create mode 100644 pymodbus/datastore/database/__init__.py rename examples/contrib/redis-datastore.py => pymodbus/datastore/database/redis_datastore.py (71%) rename examples/contrib/database-datastore.py => pymodbus/datastore/database/sql_datastore.py (80%) diff --git a/examples/common/dbstore-update-server.py b/examples/common/dbstore-update-server.py new file mode 100644 index 000000000..b0ce87bf0 --- /dev/null +++ b/examples/common/dbstore-update-server.py @@ -0,0 +1,94 @@ +''' +Pymodbus Server With Updating Thread +-------------------------------------------------------------------------- +This is an example of having a background thread updating the +context in an SQLite4 database while the server is operating. + +This scrit generates a random address range (within 0 - 65000) and a random +value and stores it in a database. It then reads the same address to verify +that the process works as expected + +This can also be done with a python thread:: + from threading import Thread + thread = Thread(target=updating_writer, args=(context,)) + thread.start() +''' +#---------------------------------------------------------------------------# +# import the modbus libraries we need +#---------------------------------------------------------------------------# +from pymodbus.server.async import StartTcpServer +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusServerContext +from pymodbus.datastore.database import SqlSlaveContext +from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer +import random + +#---------------------------------------------------------------------------# +# import the twisted libraries we need +#---------------------------------------------------------------------------# +from twisted.internet.task import LoopingCall + +#---------------------------------------------------------------------------# +# configure the service logging +#---------------------------------------------------------------------------# +import logging +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.DEBUG) + +#---------------------------------------------------------------------------# +# define your callback process +#---------------------------------------------------------------------------# +def updating_writer(a): + ''' A worker process that runs every so often and + updates live values of the context which resides in an SQLite3 database. + It should be noted that there is a race condition for the update. + :param arguments: The input arguments to the call + ''' + log.debug("Updating the database context") + context = a[0] + readfunction = 0x03 # read holding registers + writefunction = 0x10 + slave_id = 0x01 # slave address + count = 50 + + # import pdb; pdb.set_trace() + + rand_value = random.randint(0, 9999) + rand_addr = random.randint(0, 65000) + log.debug("Writing to datastore: {}, {}".format(rand_addr, rand_value)) + # import pdb; pdb.set_trace() + context[slave_id].setValues(writefunction, rand_addr, [rand_value]) + values = context[slave_id].getValues(readfunction, rand_addr, count) + log.debug("Values from datastore: " + str(values)) + + + +#---------------------------------------------------------------------------# +# initialize your data store +#---------------------------------------------------------------------------# +block = ModbusSequentialDataBlock(0x00, [0]*0xff) +store = SqlSlaveContext(block) + +context = ModbusServerContext(slaves={1: store}, single=False) + + +#---------------------------------------------------------------------------# +# initialize the server information +#---------------------------------------------------------------------------# +identity = ModbusDeviceIdentification() +identity.VendorName = 'pymodbus' +identity.ProductCode = 'PM' +identity.VendorUrl = 'http://github.com/bashwork/pymodbus/' +identity.ProductName = 'pymodbus Server' +identity.ModelName = 'pymodbus Server' +identity.MajorMinorRevision = '1.0' + +#---------------------------------------------------------------------------# +# run the server you want +#---------------------------------------------------------------------------# +time = 5 # 5 seconds delay +loop = LoopingCall(f=updating_writer, a=(context,)) +loop.start(time, now=False) # initially delay by time +StartTcpServer(context, identity=identity, address=("", 5020)) diff --git a/examples/contrib/message-generator.py b/examples/contrib/message-generator.py index b9a1e8f0a..51146434b 100755 --- a/examples/contrib/message-generator.py +++ b/examples/contrib/message-generator.py @@ -12,6 +12,7 @@ * binary - `./generate-messages.py -f binary -m tx -b` ''' from optparse import OptionParser +import codecs as c #--------------------------------------------------------------------------# # import all the available framers #--------------------------------------------------------------------------# @@ -30,6 +31,7 @@ from pymodbus.mei_message import * from pymodbus.register_read_message import * from pymodbus.register_write_message import * +from pymodbus.compat import IS_PYTHON3 #--------------------------------------------------------------------------# # initialize logging @@ -51,17 +53,17 @@ WriteSingleRegisterRequest, WriteSingleCoilRequest, ReadWriteMultipleRegistersRequest, - + ReadExceptionStatusRequest, GetCommEventCounterRequest, GetCommEventLogRequest, ReportSlaveIdRequest, - + ReadFileRecordRequest, WriteFileRecordRequest, MaskWriteRegisterRequest, ReadFifoQueueRequest, - + ReadDeviceInformationRequest, ReturnQueryDataRequest, @@ -97,7 +99,7 @@ WriteSingleRegisterResponse, WriteSingleCoilResponse, ReadWriteMultipleRegistersResponse, - + ReadExceptionStatusResponse, GetCommEventCounterResponse, GetCommEventLogResponse, @@ -149,13 +151,13 @@ 'write_registers' : [0x01] * 8, 'transaction' : 0x01, 'protocol' : 0x00, - 'unit' : 0x01, + 'unit' : 0xff, } -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # generate all the requested messages -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# def generate_messages(framer, options): ''' A helper method to parse the command line options @@ -168,13 +170,16 @@ def generate_messages(framer, options): print ("%-44s = " % message.__class__.__name__) packet = framer.buildPacket(message) if not options.ascii: - packet = packet.encode('hex') + '\n' - print (packet) # because ascii ends with a \r\n + if not IS_PYTHON3: + packet = packet.encode('hex') + else: + packet = c.encode(packet, 'hex_codec').decode('utf-8') + print ("{}\n".format(packet)) # because ascii ends with a \r\n -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # initialize our program settings -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# def get_options(): ''' A helper method to parse the command line options diff --git a/examples/contrib/message-parser.py b/examples/contrib/message-parser.py index b5c653bf3..be8fc8b42 100755 --- a/examples/contrib/message-parser.py +++ b/examples/contrib/message-parser.py @@ -11,7 +11,7 @@ * rtu * binary ''' -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # import needed libraries #---------------------------------------------------------------------------# from __future__ import print_function @@ -19,12 +19,16 @@ import collections import textwrap from optparse import OptionParser +import codecs as c + from pymodbus.utilities import computeCRC, computeLRC from pymodbus.factory import ClientDecoder, ServerDecoder from pymodbus.transaction import ModbusSocketFramer from pymodbus.transaction import ModbusBinaryFramer from pymodbus.transaction import ModbusAsciiFramer from pymodbus.transaction import ModbusRtuFramer +from pymodbus.compat import byte2int, int2byte, IS_PYTHON3 + #--------------------------------------------------------------------------# # Logging @@ -33,9 +37,9 @@ modbus_log = logging.getLogger("pymodbus") -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # build a quick wrapper around the framers -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# class Decoder(object): def __init__(self, framer, encode=False): @@ -52,7 +56,10 @@ def decode(self, message): :param message: The messge to decode ''' - value = message if self.encode else message.encode('hex') + if IS_PYTHON3: + value = message if self.encode else c.encode(message, 'hex_codec') + else: + value = message if self.encode else message.encode('hex') print("="*80) print("Decoding Message %s" % value) print("="*80) @@ -64,7 +71,7 @@ def decode(self, message): print("%s" % decoder.decoder.__class__.__name__) print("-"*80) try: - decoder.addToFrame(message.encode()) + decoder.addToFrame(message) if decoder.checkFrame(): decoder.advanceFrame() decoder.processIncomingPacket(message, self.report) @@ -86,7 +93,7 @@ def report(self, message): :param message: The message to print ''' print("%-15s = %s" % ('name', message.__class__.__name__)) - for k,v in message.__dict__.iteritems(): + for (k, v) in message.__dict__.items(): if isinstance(v, dict): print("%-15s =" % k) for kk,vv in v.items(): @@ -102,9 +109,9 @@ def report(self, message): print("%-15s = %s" % ('documentation', message.__doc__)) -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # and decode our message -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# def get_options(): ''' A helper method to parse the command line options @@ -136,6 +143,10 @@ def get_options(): help="The file containing messages to parse", dest="file", default=None) + 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: @@ -150,8 +161,19 @@ def get_messages(option): :returns: The message iterator to parse ''' if option.message: + if option.transaction: + msg = "" + for segment in option.message.split(): + segment = segment.replace("0x", "") + segment = "0" + segment if len(segment) == 1 else segment + msg = msg + segment + option.message = msg + if not option.ascii: - option.message = option.message.decode('hex') + if not IS_PYTHON3: + option.message = option.message.decode('hex') + else: + option.message = c.decode(option.message.encode(), 'hex_codec') yield option.message elif option.file: with open(option.file, "r") as handle: diff --git a/pymodbus/datastore/database/__init__.py b/pymodbus/datastore/database/__init__.py new file mode 100644 index 000000000..dbb2609a4 --- /dev/null +++ b/pymodbus/datastore/database/__init__.py @@ -0,0 +1,7 @@ +from pymodbus.datastore.database.sql_datastore import SqlSlaveContext +from pymodbus.datastore.database.redis_datastore import RedisSlaveContext + +#---------------------------------------------------------------------------# +# Exported symbols +#---------------------------------------------------------------------------# +__all__ = ["SqlSlaveContext", "RedisSlaveContext"] diff --git a/examples/contrib/redis-datastore.py b/pymodbus/datastore/database/redis_datastore.py similarity index 71% rename from examples/contrib/redis-datastore.py rename to pymodbus/datastore/database/redis_datastore.py index ef44c6544..b7c74b013 100644 --- a/examples/contrib/redis-datastore.py +++ b/pymodbus/datastore/database/redis_datastore.py @@ -29,7 +29,7 @@ def __init__(self, **kwargs): port = kwargs.get('port', 6379) self.prefix = kwargs.get('prefix', 'pymodbus') self.client = kwargs.get('client', redis.Redis(host=host, port=port)) - self.__build_mapping() + self._build_mapping() def __str__(self): ''' Returns a string representation of the context @@ -52,7 +52,7 @@ def validate(self, fx, address, count=1): ''' address = address + 1 # section 4.4 of specification _logger.debug("validate[%d] %d:%d" % (fx, address, count)) - return self.__val_callbacks[self.decode(fx)](address, count) + return self._val_callbacks[self.decode(fx)](address, count) def getValues(self, fx, address, count=1): ''' Validates the request to make sure it is in range @@ -64,7 +64,7 @@ def getValues(self, fx, address, count=1): ''' address = address + 1 # section 4.4 of specification _logger.debug("getValues[%d] %d:%d" % (fx, address, count)) - return self.__get_callbacks[self.decode(fx)](address, count) + return self._get_callbacks[self.decode(fx)](address, count) def setValues(self, fx, address, values): ''' Sets the datastore with the supplied values @@ -75,12 +75,12 @@ def setValues(self, fx, address, values): ''' address = address + 1 # section 4.4 of specification _logger.debug("setValues[%d] %d:%d" % (fx, address, len(values))) - self.__set_callbacks[self.decode(fx)](address, values) + self._set_callbacks[self.decode(fx)](address, values) #--------------------------------------------------------------------------# # Redis Helper Methods #--------------------------------------------------------------------------# - def __get_prefix(self, key): + def _get_prefix(self, key): ''' This is a helper to abstract getting bit values :param key: The key prefix to use @@ -88,52 +88,52 @@ def __get_prefix(self, key): ''' return "%s:%s" % (self.prefix, key) - def __build_mapping(self): + def _build_mapping(self): ''' A quick helper method to build the function code mapper. ''' - self.__val_callbacks = { - 'd' : lambda o, c: self.__val_bit('d', o, c), - 'c' : lambda o, c: self.__val_bit('c', o, c), - 'h' : lambda o, c: self.__val_reg('h', o, c), - 'i' : lambda o, c: self.__val_reg('i', o, c), + self._val_callbacks = { + 'd' : lambda o, c: self._val_bit('d', o, c), + 'c' : lambda o, c: self._val_bit('c', o, c), + 'h' : lambda o, c: self._val_reg('h', o, c), + 'i' : lambda o, c: self._val_reg('i', o, c), } - self.__get_callbacks = { - 'd' : lambda o, c: self.__get_bit('d', o, c), - 'c' : lambda o, c: self.__get_bit('c', o, c), - 'h' : lambda o, c: self.__get_reg('h', o, c), - 'i' : lambda o, c: self.__get_reg('i', o, c), + self._get_callbacks = { + 'd' : lambda o, c: self._get_bit('d', o, c), + 'c' : lambda o, c: self._get_bit('c', o, c), + 'h' : lambda o, c: self._get_reg('h', o, c), + 'i' : lambda o, c: self._get_reg('i', o, c), } - self.__set_callbacks = { - 'd' : lambda o, v: self.__set_bit('d', o, v), - 'c' : lambda o, v: self.__set_bit('c', o, v), - 'h' : lambda o, v: self.__set_reg('h', o, v), - 'i' : lambda o, v: self.__set_reg('i', o, v), + self._set_callbacks = { + 'd' : lambda o, v: self._set_bit('d', o, v), + 'c' : lambda o, v: self._set_bit('c', o, v), + 'h' : lambda o, v: self._set_reg('h', o, v), + 'i' : lambda o, v: self._set_reg('i', o, v), } #--------------------------------------------------------------------------# # Redis discrete implementation #--------------------------------------------------------------------------# - __bit_size = 16 - __bit_default = '\x00' * (__bit_size % 8) + _bit_size = 16 + _bit_default = '\x00' * (_bit_size % 8) - def __get_bit_values(self, key, offset, count): + def _get_bit_values(self, key, offset, count): ''' This is a helper to abstract getting bit values :param key: The key prefix to use :param offset: The address offset to start at :param count: The number of bits to read ''' - key = self.__get_prefix(key) - s = divmod(offset, self.__bit_size)[0] - e = divmod(offset + count, self.__bit_size)[0] + key = self._get_prefix(key) + s = divmod(offset, self._bit_size)[0] + e = divmod(offset + count, self._bit_size)[0] request = ('%s:%s' % (key, v) for v in range(s, e + 1)) response = self.client.mget(request) return response - def __val_bit(self, key, offset, count): + def _val_bit(self, key, offset, count): ''' Validates that the given range is currently set in redis. If any of the keys return None, then it is invalid. @@ -141,23 +141,23 @@ def __val_bit(self, key, offset, count): :param offset: The address offset to start at :param count: The number of bits to read ''' - response = self.__get_bit_values(key, offset, count) - return None not in response + response = self._get_bit_values(key, offset, count) + return True if None not in response else False - def __get_bit(self, key, offset, count): + def _get_bit(self, key, offset, count): ''' :param key: The key prefix to use :param offset: The address offset to start at :param count: The number of bits to read ''' - response = self.__get_bit_values(key, offset, count) - response = (r or self.__bit_default for r in response) + response = self._get_bit_values(key, offset, count) + response = (r or self._bit_default for r in response) result = ''.join(response) result = unpack_bitstring(result) return result[offset:offset + count] - def __set_bit(self, key, offset, values): + def _set_bit(self, key, offset, values): ''' :param key: The key prefix to use @@ -165,17 +165,17 @@ def __set_bit(self, key, offset, values): :param values: The values to set ''' count = len(values) - s = divmod(offset, self.__bit_size)[0] - e = divmod(offset + count, self.__bit_size)[0] + s = divmod(offset, self._bit_size)[0] + e = divmod(offset + count, self._bit_size)[0] value = pack_bitstring(values) - current = self.__get_bit_values(key, offset, count) - current = (r or self.__bit_default for r in current) + current = self._get_bit_values(key, offset, count) + current = (r or self._bit_default for r in current) current = ''.join(current) - current = current[0:offset] + value + current[offset + count:] - final = (current[s:s + self.__bit_size] for s in range(0, count, self.__bit_size)) + current = current[0:offset] + value.decode('utf-8') + current[offset + count:] + final = (current[s:s + self._bit_size] for s in range(0, count, self._bit_size)) - key = self.__get_prefix(key) + key = self._get_prefix(key) request = ('%s:%s' % (key, v) for v in range(s, e + 1)) request = dict(zip(request, final)) self.client.mset(request) @@ -183,17 +183,17 @@ def __set_bit(self, key, offset, values): #--------------------------------------------------------------------------# # Redis register implementation #--------------------------------------------------------------------------# - __reg_size = 16 - __reg_default = '\x00' * (__reg_size % 8) + _reg_size = 16 + _reg_default = '\x00' * (_reg_size % 8) - def __get_reg_values(self, key, offset, count): + def _get_reg_values(self, key, offset, count): ''' This is a helper to abstract getting register values :param key: The key prefix to use :param offset: The address offset to start at :param count: The number of bits to read ''' - key = self.__get_prefix(key) + key = self._get_prefix(key) #s = divmod(offset, self.__reg_size)[0] #e = divmod(offset+count, self.__reg_size)[0] @@ -202,7 +202,7 @@ def __get_reg_values(self, key, offset, count): response = self.client.mget(request) return response - def __val_reg(self, key, offset, count): + def _val_reg(self, key, offset, count): ''' Validates that the given range is currently set in redis. If any of the keys return None, then it is invalid. @@ -210,21 +210,21 @@ def __val_reg(self, key, offset, count): :param offset: The address offset to start at :param count: The number of bits to read ''' - response = self.__get_reg_values(key, offset, count) + response = self._get_reg_values(key, offset, count) return None not in response - def __get_reg(self, key, offset, count): + def _get_reg(self, key, offset, count): ''' :param key: The key prefix to use :param offset: The address offset to start at :param count: The number of bits to read ''' - response = self.__get_reg_values(key, offset, count) - response = [r or self.__reg_default for r in response] + response = self._get_reg_values(key, offset, count) + response = [r or self._reg_default for r in response] return response[offset:offset + count] - def __set_reg(self, key, offset, values): + def _set_reg(self, key, offset, values): ''' :param key: The key prefix to use @@ -237,7 +237,7 @@ def __set_reg(self, key, offset, values): #current = self.__get_reg_values(key, offset, count) - key = self.__get_prefix(key) + key = self._get_prefix(key) request = ('%s:%s' % (key, v) for v in range(offset, count + 1)) request = dict(zip(request, values)) self.client.mset(request) diff --git a/examples/contrib/database-datastore.py b/pymodbus/datastore/database/sql_datastore.py similarity index 80% rename from examples/contrib/database-datastore.py rename to pymodbus/datastore/database/sql_datastore.py index c1c48b161..a02894251 100644 --- a/examples/contrib/database-datastore.py +++ b/pymodbus/datastore/database/sql_datastore.py @@ -17,7 +17,7 @@ #---------------------------------------------------------------------------# # Context #---------------------------------------------------------------------------# -class DatabaseSlaveContext(IModbusSlaveContext): +class SqlSlaveContext(IModbusSlaveContext): ''' This creates a modbus data model with each data access stored in its own personal block @@ -30,7 +30,7 @@ def __init__(self, *args, **kwargs): ''' self.table = kwargs.get('table', 'pymodbus') self.database = kwargs.get('database', 'sqlite:///pymodbus.db') - self.__db_create(self.table, self.database) + self._db_create(self.table, self.database) def __str__(self): ''' Returns a string representation of the context @@ -42,8 +42,7 @@ def __str__(self): def reset(self): ''' Resets all the datastores to their default values ''' self._metadata.drop_all() - self.__db_create(self.table, self.database) - raise NotImplementedException() # TODO drop table? + self._db_create(self.table, self.database) def validate(self, fx, address, count=1): ''' Validates the request to make sure it is in range @@ -55,7 +54,7 @@ def validate(self, fx, address, count=1): ''' address = address + 1 # section 4.4 of specification _logger.debug("validate[%d] %d:%d" % (fx, address, count)) - return self.__validate(self.decode(fx), address, count) + return self._validate(self.decode(fx), address, count) def getValues(self, fx, address, count=1): ''' Validates the request to make sure it is in range @@ -67,7 +66,7 @@ def getValues(self, fx, address, count=1): ''' address = address + 1 # section 4.4 of specification _logger.debug("get-values[%d] %d:%d" % (fx, address, count)) - return self.__get(self.decode(fx), address, count) + return self._get(self.decode(fx), address, count) def setValues(self, fx, address, values): ''' Sets the datastore with the supplied values @@ -78,12 +77,12 @@ def setValues(self, fx, address, values): ''' address = address + 1 # section 4.4 of specification _logger.debug("set-values[%d] %d:%d" % (fx, address, len(values))) - self.__set(self.decode(fx), address, values) + self._set(self.decode(fx), address, values) #--------------------------------------------------------------------------# # Sqlite Helper Methods #--------------------------------------------------------------------------# - def __db_create(self, table, database): + def _db_create(self, table, database): ''' A helper method to initialize the database and handles :param table: The table name to create @@ -99,9 +98,8 @@ def __db_create(self, table, database): self._table.create(checkfirst=True) self._connection = self._engine.connect() - def __get(self, type, offset, count): + def _get(self, type, offset, count): ''' - :param type: The key prefix to use :param offset: The address offset to start at :param count: The number of bits to read @@ -110,47 +108,56 @@ def __get(self, type, offset, count): query = self._table.select(and_( self._table.c.type == type, self._table.c.index >= offset, - self._table.c.index <= offset + count)) + self._table.c.index <= offset + count) + ) query = query.order_by(self._table.c.index.asc()) result = self._connection.execute(query).fetchall() return [row.value for row in result] - def __build_set(self, type, offset, values, p=''): + def _build_set(self, type, offset, values, prefix=''): ''' A helper method to generate the sql update context :param type: The key prefix to use :param offset: The address offset to start at :param values: The values to set + :param prefix: Prefix fields index and type, defaults to empty string ''' result = [] for index, value in enumerate(values): result.append({ - p + 'type' : type, - p + 'index' : offset + index, + prefix + 'type' : type, + prefix + 'index' : offset + index, 'value' : value }) return result - def __set(self, type, offset, values): + def _check(self, type, offset, values): + result = self._get(type, offset, count=1) + return False if len(result) > 0 else True + + def _set(self, type, offset, values): ''' :param key: The type prefix to use :param offset: The address offset to start at :param values: The values to set ''' - context = self.__build_set(type, offset, values) - query = self._table.insert() - result = self._connection.execute(query, context) - return result.rowcount == len(values) - - def __update(self, type, offset, values): + if self._check(type, offset, values): + context = self._build_set(type, offset, values) + query = self._table.insert() + result = self._connection.execute(query, context) + return result.rowcount == len(values) + else: + return False + + def _update(self, type, offset, values): ''' :param type: The type prefix to use :param offset: The address offset to start at :param values: The values to set ''' - context = self.__build_set(type, offset, values, p='x_') + context = self._build_set(type, offset, values, prefix='x_') query = self._table.update().values(name='value') query = query.where(and_( self._table.c.type == bindparam('x_type'), @@ -158,7 +165,7 @@ def __update(self, type, offset, values): result = self._connection.execute(query, context) return result.rowcount == len(values) - def __validate(self, key, offset, count): + def _validate(self, type, offset, count): ''' :param key: The key prefix to use :param offset: The address offset to start at diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index f4beeea91..78ec598bc 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -102,7 +102,10 @@ def handle(self): if data: if _logger.isEnabledFor(logging.DEBUG): _logger.debug(" ".join([hex(byte2int(x)) for x in data])) - unit_address = byte2int(data[0]) + if not isinstance(self.framer, ModbusBinaryFramer): + unit_address = byte2int(data[0]) + else: + unit_address = byte2int(data[1]) if unit_address in self.server.context: self.framer.processIncomingPacket(data, self.execute) except Exception as msg: @@ -273,13 +276,14 @@ def __init__(self, context, framer=None, identity=None, address=None, handler=No self.context = context or ModbusServerContext() self.control = ModbusControlBlock() self.address = address or ("", Defaults.Port) + self.handler = handler or ModbusConnectedRequestHandler self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', Defaults.IgnoreMissingSlaves) if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) socketserver.ThreadingTCPServer.__init__(self, - self.address, ModbusConnectedRequestHandler) + self.address, self.handler) def process_request(self, request, client): ''' Callback for connecting a new client thread @@ -336,13 +340,14 @@ def __init__(self, context, framer=None, identity=None, address=None, handler=No self.context = context or ModbusServerContext() self.control = ModbusControlBlock() self.address = address or ("", Defaults.Port) + self.handler = handler or ModbusDisconnectedRequestHandler self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', Defaults.IgnoreMissingSlaves) if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) socketserver.ThreadingUDPServer.__init__(self, - self.address, ModbusDisconnectedRequestHandler) + self.address, self.handler) def process_request(self, request, client): ''' Callback for connecting a new client thread diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 1efd3c17f..cc44be438 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -461,7 +461,7 @@ def processIncomingPacket(self, data, callback): def _process(self, callback, error=False): """ - Process incoming packets irrespective error condition + Process incoming packets irrespective error condition """ data = self.getRawFrame() if error else self.getFrame() result = self.decoder.decode(data) @@ -487,7 +487,7 @@ def resetFrame(self): def getRawFrame(self): """ - Returns the complete buffer + Returns the complete buffer """ return self.__buffer @@ -922,7 +922,7 @@ def __init__(self, decoder): ''' self.__buffer = b'' self.__header = {'crc':0x0000, 'len':0, 'uid':0x00} - self.__hsize = 0x02 + self.__hsize = 0x01 self.__start = b'\x7b' # { self.__end = b'\x7d' # } self.__repeat = [b'}'[0], b'{'[0]] # python3 hack diff --git a/pymodbus/utilities.py b/pymodbus/utilities.py index e3ef421e2..a15515acf 100644 --- a/pymodbus/utilities.py +++ b/pymodbus/utilities.py @@ -86,7 +86,10 @@ def unpack_bitstring(string): byte_count = len(string) bits = [] for byte in range(byte_count): - value = byte2int(string[byte]) + if IS_PYTHON3: + value = byte2int(int(string[byte])) + else: + value = byte2int(string[byte]) for _ in range(8): bits.append((value & 1) == 1) value >>= 1 @@ -96,8 +99,8 @@ def unpack_bitstring(string): def make_byte_string(s): """ Returns byte string from a given string, python3 specific fix - :param s: - :return: + :param s: + :return: """ if IS_PYTHON3 and isinstance(s, string_types): s = s.encode() diff --git a/requirements-tests.txt b/requirements-tests.txt index 85623bb57..5c4639d1b 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -8,5 +8,8 @@ Twisted>=17.1.0 zope.interface>=4.4.0 pyasn1>=0.2.3 pycrypto>=2.6.1 +pyserial>=3.4 +redis>=2.10.5 +sqlalchemy>=1.1.15 #wsgiref>=0.1.2 cryptography>=1.8.1 \ No newline at end of file diff --git a/test/test_datastore.py b/test/test_datastore.py index b6b401517..c1d99c33c 100644 --- a/test/test_datastore.py +++ b/test/test_datastore.py @@ -1,7 +1,13 @@ #!/usr/bin/env python import unittest +import mock +from mock import MagicMock +import redis +import random from pymodbus.datastore import * from pymodbus.datastore.store import BaseModbusDataBlock +from pymodbus.datastore.database import SqlSlaveContext +from pymodbus.datastore.database import RedisSlaveContext from pymodbus.exceptions import NotImplementedException from pymodbus.exceptions import NoSuchSlaveException from pymodbus.exceptions import ParameterException @@ -113,7 +119,7 @@ def testModbusSlaveContext(self): } context = ModbusSlaveContext(**store) self.assertNotEqual(str(context), None) - + for fx in [1,2,3,4]: context.setValues(fx, 0, [True]*10) self.assertTrue(context.validate(fx, 0,10)) @@ -132,6 +138,236 @@ def _set(ctx): self.assertRaises(NoSuchSlaveException, lambda: _set(context)) self.assertRaises(NoSuchSlaveException, lambda: context[0xffff]) + +class RedisDataStoreTest(unittest.TestCase): + ''' + This is the unittest for the pymodbus.datastore.database.redis module + ''' + + def setUp(self): + self.slave = RedisSlaveContext() + + def tearDown(self): + ''' Cleans up the test environment ''' + pass + + def testStr(self): + # slave = RedisSlaveContext() + self.assertEqual(str(self.slave), "Redis Slave Context %s" % self.slave.client) + + def testReset(self): + assert isinstance(self.slave.client, redis.Redis) + self.slave.client = MagicMock() + self.slave.reset() + self.slave.client.flushall.assert_called_once_with() + + def testValCallbacksSuccess(self): + self.slave._build_mapping() + mock_count = 3 + mock_offset = 0 + self.slave.client.mset = MagicMock() + self.slave.client.mget = MagicMock(return_value=['11']) + + for key in ('d', 'c', 'h', 'i'): + self.assertTrue( + self.slave._val_callbacks[key](mock_offset, mock_count) + ) + + def testValCallbacksFailure(self): + self.slave._build_mapping() + mock_count = 3 + mock_offset = 0 + self.slave.client.mset = MagicMock() + self.slave.client.mget = MagicMock(return_value=['11', None]) + + for key in ('d', 'c', 'h', 'i'): + self.assertFalse( + self.slave._val_callbacks[key](mock_offset, mock_count) + ) + + def testGetCallbacks(self): + self.slave._build_mapping() + mock_count = 3 + mock_offset = 0 + self.slave.client.mget = MagicMock(return_value='11') + + for key in ('d', 'c'): + resp = self.slave._get_callbacks[key](mock_offset, mock_count) + self.assertEqual(resp, [True, False, False]) + + for key in ('h', 'i'): + resp = self.slave._get_callbacks[key](mock_offset, mock_count) + self.assertEqual(resp, ['1', '1']) + + def testSetCallbacks(self): + self.slave._build_mapping() + mock_values = [3] + mock_offset = 0 + self.slave.client.mset = MagicMock() + self.slave.client.mget = MagicMock() + + for key in ['c', 'd']: + self.slave._set_callbacks[key](mock_offset, [3]) + k = "pymodbus:{}:{}".format(key, mock_offset) + self.slave.client.mset.assert_called_with( + {k: '\x01'} + ) + + for key in ('h', 'i'): + self.slave._set_callbacks[key](mock_offset, [3]) + k = "pymodbus:{}:{}".format(key, mock_offset) + self.slave.client.mset.assert_called_with( + {k: mock_values[0]} + ) + + def testValidate(self): + self.slave.client.mget = MagicMock(return_value=[123]) + self.assertTrue(self.slave.validate(0x01, 3000)) + + def testSetValue(self): + self.slave.client.mset = MagicMock() + self.slave.client.mget = MagicMock() + self.assertEqual(self.slave.setValues(0x01, 1000, [12]), None) + + def testGetValue(self): + self.slave.client.mget = MagicMock(return_value=["123"]) + self.assertEqual(self.slave.getValues(0x01, 23), []) + + +class MockSqlResult(object): + def __init__(self, rowcount=0, value=0): + self.rowcount = rowcount + self.value = value + + +class SqlDataStoreTest(unittest.TestCase): + ''' + This is the unittest for the pymodbus.datastore.database.SqlSlaveContesxt + module + ''' + + def setUp(self): + self.slave = SqlSlaveContext() + self.slave._metadata.drop_all = MagicMock() + self.slave._db_create = MagicMock() + self.slave._table.select = MagicMock() + self.slave._connection = MagicMock() + + self.mock_addr = random.randint(0, 65000) + self.mock_values = random.sample(range(1, 100), 5) + self.mock_function = 0x01 + self.mock_type = 'h' + self.mock_offset = 0 + self.mock_count = 1 + + self.function_map = {2: 'd', 4: 'i'} + self.function_map.update([(i, 'h') for i in [3, 6, 16, 22, 23]]) + self.function_map.update([(i, 'c') for i in [1, 5, 15]]) + + def tearDown(self): + ''' Cleans up the test environment ''' + pass + + def testStr(self): + self.assertEqual(str(self.slave), "Modbus Slave Context") + + def testReset(self): + self.slave.reset() + + self.slave._metadata.drop_all.assert_called_once_with() + self.slave._db_create.assert_called_once_with( + self.slave.table, self.slave.database + ) + def testValidateSuccess(self): + mock_result = MockSqlResult( + rowcount=len(self.mock_values) + ) + self.slave._connection.execute = MagicMock(return_value=mock_result) + self.assertTrue(self.slave.validate( + self.mock_function, self.mock_addr, len(self.mock_values)) + ) + + def testValidateFailure(self): + wrong_count = 9 + mock_result = MockSqlResult(rowcount=len(self.mock_values)) + self.slave._connection.execute = MagicMock(return_value=mock_result) + self.assertFalse(self.slave.validate( + self.mock_function, self.mock_addr, wrong_count) + ) + + def testBuildSet(self): + mock_set = [ + { + 'index': 0, + 'type': 'h', + 'value': 11 + }, + { + 'index': 1, + 'type': 'h', + 'value': 12 + } + ] + self.assertListEqual(self.slave._build_set('h', 0, [11, 12]), mock_set) + + def testCheckSuccess(self): + mock_success_results = [1, 2, 3] + self.slave._get = MagicMock(return_value=mock_success_results) + self.assertFalse(self.slave._check('h', 0, 1)) + + def testCheckFailure(self): + mock_success_results = [] + self.slave._get = MagicMock(return_value=mock_success_results) + self.assertTrue(self.slave._check('h', 0, 1)) + + def testGetValues(self): + self.slave._get = MagicMock() + + for key, value in self.function_map.items(): + self.slave.getValues(key, self.mock_addr, self.mock_count) + self.slave._get.assert_called_with( + value, self.mock_addr + 1, self.mock_count + ) + + def testSetValues(self): + self.slave._set = MagicMock() + + for key, value in self.function_map.items(): + self.slave.setValues(key, self.mock_addr, self.mock_values) + self.slave._set.assert_called_with( + value, self.mock_addr + 1, self.mock_values + ) + + def testSet(self): + self.slave._check = MagicMock(return_value=True) + self.slave._connection.execute = MagicMock( + return_value=MockSqlResult(rowcount=len(self.mock_values)) + ) + self.assertTrue(self.slave._set( + self.mock_type, self.mock_offset, self.mock_values) + ) + + self.slave._check = MagicMock(return_value=False) + self.assertFalse( + self.slave._set(self.mock_type, self.mock_offset, self.mock_values) + ) + + def testUpdateSuccess(self): + self.slave._connection.execute = MagicMock( + return_value=MockSqlResult(rowcount=len(self.mock_values)) + ) + self.assertTrue( + self.slave._update(self.mock_type, self.mock_offset, self.mock_values) + ) + + def testUpdateFailure(self): + self.slave._connection.execute = MagicMock( + return_value=MockSqlResult(rowcount=100) + ) + self.assertFalse( + self.slave._update(self.mock_type, self.mock_offset, self.mock_values) + ) + #---------------------------------------------------------------------------# # Main #---------------------------------------------------------------------------# diff --git a/test/test_transaction.py b/test/test_transaction.py index 7a90ed165..8c5e18f47 100644 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -32,9 +32,9 @@ def tearDown(self): del self._rtu del self._ascii - #---------------------------------------------------------------------------# + #---------------------------------------------------------------------------# # Dictionary based transaction manager - #---------------------------------------------------------------------------# + #---------------------------------------------------------------------------# def testDictTransactionManagerTID(self): ''' Test the dict transaction manager TID ''' for tid in range(1, self._manager.getNextTID() + 10): @@ -65,9 +65,9 @@ class Request: pass self._manager.delTransaction(handle.transaction_id) self.assertEqual(None, self._manager.getTransaction(handle.transaction_id)) - #---------------------------------------------------------------------------# + #---------------------------------------------------------------------------# # Queue based transaction manager - #---------------------------------------------------------------------------# + #---------------------------------------------------------------------------# def testFifoTransactionManagerTID(self): ''' Test the fifo transaction manager TID ''' for tid in range(1, self._queue_manager.getNextTID() + 10): @@ -98,7 +98,7 @@ class Request: pass self._queue_manager.delTransaction(handle.transaction_id) self.assertEqual(None, self._queue_manager.getTransaction(handle.transaction_id)) - #---------------------------------------------------------------------------# + #---------------------------------------------------------------------------# # TCP tests #---------------------------------------------------------------------------# def testTCPFramerTransactionReady(self): @@ -361,7 +361,7 @@ def testBinaryFramerTransactionReady(self): def testBinaryFramerTransactionFull(self): ''' Test a full binary frame transaction ''' msg = b'\x7b\x01\x03\x00\x00\x00\x05\x85\xC9\x7d' - pack = msg[3:-3] + pack = msg[2:-3] self._binary.addToFrame(msg) self.assertTrue(self._binary.checkFrame()) result = self._binary.getFrame() @@ -372,7 +372,7 @@ def testBinaryFramerTransactionHalf(self): ''' Test a half completed binary frame transaction ''' msg1 = b'\x7b\x01\x03\x00' msg2 = b'\x00\x00\x05\x85\xC9\x7d' - pack = msg1[3:] + msg2[:-3] + pack = msg1[2:] + msg2[:-3] self._binary.addToFrame(msg1) self.assertFalse(self._binary.checkFrame()) result = self._binary.getFrame() From b626aec5dc3086111c8890a4d7dec8476bd2b031 Mon Sep 17 00:00:00 2001 From: rahul Date: Wed, 29 Nov 2017 12:44:40 +0530 Subject: [PATCH 02/15] #245: Unitests coverage of async server upto 94% --- pymodbus/server/async.py | 2 +- test/test_server_async.py | 218 ++++++++++++++++++++++++++++++++------ 2 files changed, 189 insertions(+), 31 deletions(-) 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/test/test_server_async.py b/test/test_server_async.py index 89344f73c..6e36098bf 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,110 @@ 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!" + protocol.factory = MagicMock() + protocol.factory.control.ListenOnly = False + protocol.factory.store = [byte2int(mock_data[0])] + protocol.framer = protocol._execute = MagicMock() + protocol.dataReceived(mock_data) + protocol.framer.processIncomingPacket.assert_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) + protocol._send.assert_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) + ) + request.doException.assert_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) + ) + protocol._send.assert_called() + + def testSendTcp(self): + + class MockMsg(object): + def __init__(self, msg, resp=False): + self.should_respond = resp + self.msg = msg + + protocol = ModbusTcpProtocol() + mock_data =MockMsg(resp=True, msg="helloworld") + + protocol.control = MagicMock() + protocol.framer = MagicMock() + protocol.factory = MagicMock() + protocol.framer.buildPacket = MagicMock(return_value='a') + protocol.transport= MagicMock() + + protocol._send(mock_data) + + protocol.framer.buildPacket.assert_called_with(mock_data) + protocol.transport.write.assert_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 +159,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 +169,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 +183,82 @@ def testSerialServerStartup(self): StartSerialServer(context=None, port=SERIAL_PORT) self.assertEqual(mock_reactor.run.call_count, 1) + def testDatagramReceived(self): + mock_data = "hello world" + mock_addr = 0x01 + protocol = ModbusUdpProtocol(store=None) + protocol.framer.processIncomingPacket = MagicMock() + protocol.control.ListenOnly = False + protocol._execute = MagicMock() + + protocol.datagramReceived(mock_data, mock_addr) + protocol.framer.processIncomingPacket.assert_called() + + def testSendUdp(self): + protocol = ModbusUdpProtocol(store=None) + mock_data = "hello world" + mock_addr = 0x01 + + protocol.control = MagicMock() + protocol.framer = MagicMock() + protocol.framer.buildPacket = MagicMock(return_value='a') + protocol.transport= MagicMock() + + protocol._send(mock_data, mock_addr) + + protocol.framer.buildPacket.assert_called_with(mock_data) + protocol.transport.write.assert_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) + protocol._send.assert_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) + ) + request.doException.assert_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) + ) + protocol._send.assert_called() + + def testStopServer(self): + from twisted.internet import reactor + reactor.stop = MagicMock() + StopServer() + + reactor.stop.assert_called() + + def testIsMainThread(self): + import threading + self.assertTrue(_is_main_thread()) + + + #---------------------------------------------------------------------------# # Main #---------------------------------------------------------------------------# From febf8bcb07a579fd720c90779d9527f3800117e7 Mon Sep 17 00:00:00 2001 From: rahul Date: Mon, 4 Dec 2017 10:46:59 +0530 Subject: [PATCH 03/15] #245: Test cverage up-to-date to 90% --- examples/contrib/message-generator.py | 6 +- pymodbus.db | Bin 0 -> 12288 bytes pymodbus/pdu.py | 4 +- pymodbus/transaction.py | 219 +++++++++++++------------- test/test_transaction.py | 183 ++++++++++++++++++++- 5 files changed, 297 insertions(+), 115 deletions(-) create mode 100644 pymodbus.db diff --git a/examples/contrib/message-generator.py b/examples/contrib/message-generator.py index 51146434b..9b6df7f45 100755 --- a/examples/contrib/message-generator.py +++ b/examples/contrib/message-generator.py @@ -32,14 +32,14 @@ from pymodbus.register_read_message import * from pymodbus.register_write_message import * from pymodbus.compat import IS_PYTHON3 - +from pymodbus.pdu import ExceptionResponse #--------------------------------------------------------------------------# # initialize logging #--------------------------------------------------------------------------# import logging modbus_log = logging.getLogger("pymodbus") - +exp = ExceptionResponse(0x01, 0x0B) #--------------------------------------------------------------------------# # enumerate all request messages #--------------------------------------------------------------------------# @@ -83,6 +83,7 @@ ReturnIopOverrunCountRequest, ClearOverrunCountRequest, GetClearModbusPlusRequest, + exp ] @@ -129,6 +130,7 @@ ReturnIopOverrunCountResponse, ClearOverrunCountResponse, GetClearModbusPlusResponse, + exp ] diff --git a/pymodbus.db b/pymodbus.db new file mode 100644 index 0000000000000000000000000000000000000000..a1ecc8efdf352a7065b45fcc7753be0dfa5426e5 GIT binary patch literal 12288 zcmeI#!AiqG5C-7gC~5;GJ%~L8hMW>8k>U#&!=i-}X_|TsDP5tMnragv~lz z=G8{8Mi`4A5tIaXV?nw~uGuN;wWOyY4j+Qp9eM*YJI?ZTAwL}&MTxib-)nIKp78_<6X&-AX1vwmoE*7n@I<;|Hs&8o6!^HhIHI@DL{ zTz{cFyK3sG2>}5JKmY;|fB*y_009U<00Izzz_ALLYGXM6AM5YMz90Yr2tWV=5P$## NAOHafKmY=*z#qktM-%`6 literal 0 HcmV?d00001 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/transaction.py b/pymodbus/transaction.py index cc44be438..2b8101c3d 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 @@ -135,7 +134,8 @@ def execute(self, request): continue if _logger.isEnabledFor(logging.DEBUG): - _logger.debug("recv: " + " ".join([hex(byte2int(x)) for x in result])) + _test_msg = " ".join([hex(byte2int(x)) for x in result]) + _logger.debug("recv: {}".format(_test_msg)) self.client.framer.processIncomingPacket(result, self.addTransaction) break except (socket.error, ModbusIOException, InvalidMessageRecievedException) as msg: @@ -356,9 +356,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 #-----------------------------------------------------------------------# @@ -368,16 +368,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 @@ -388,9 +388,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 @@ -399,22 +399,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): ''' @@ -423,9 +423,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 @@ -453,9 +453,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 @@ -482,14 +482,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 @@ -548,11 +548,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 #-----------------------------------------------------------------------# @@ -565,9 +565,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): @@ -580,11 +580,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. @@ -594,8 +594,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 @@ -604,24 +604,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): ''' @@ -630,16 +630,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 '' @@ -651,7 +651,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 @@ -678,13 +678,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 @@ -719,7 +719,7 @@ def getRawFrame(self): """ Returns the complete buffer """ - return self.__buffer + return self._buffer @@ -747,11 +747,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 #-----------------------------------------------------------------------# @@ -762,19 +762,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): @@ -783,8 +783,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 @@ -793,7 +793,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 @@ -802,16 +802,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'' @@ -823,8 +823,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 @@ -834,7 +834,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 @@ -857,7 +857,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) @@ -879,11 +880,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() @@ -920,12 +921,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 #-----------------------------------------------------------------------# @@ -936,18 +937,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): @@ -956,8 +957,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 @@ -966,7 +967,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 @@ -975,16 +976,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'' @@ -996,7 +997,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 @@ -1039,7 +1040,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): @@ -1053,7 +1054,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) @@ -1066,8 +1067,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_transaction.py b/test/test_transaction.py index 8c5e18f47..41a600a8d 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 ''' @@ -32,6 +41,115 @@ def tearDown(self): del self._rtu del self._ascii + def testCalculateResponseLength(self): + mock_expected_pdu_size = 10 + + # test returns None when base adu size = -1 + self._tm.base_adu_size = -1 + self.assertEqual( + self._tm._calculate_response_length(mock_expected_pdu_size), None + ) + + # test returns value when base adu size is > 0 + self._tm.base_adu_size = 10 + self.assertEqual( + self._tm._calculate_response_length(mock_expected_pdu_size), 20 + ) + + def testCalculateExceptionLength(self): + mock_adu_size = 10 + self._tm.base_adu_size = mock_adu_size + self._tm.client = MagicMock() + + self._tm.client.framer = ModbusSocketFramer(self.decoder) + self.assertEqual( + self._tm._calculate_exception_length(), 12 + ) + + self._tm.client.framer = ModbusRtuFramer(self.decoder) + self.assertEqual( + self._tm._calculate_exception_length(), 12 + ) + + self._tm.client.framer = ModbusBinaryFramer(self.decoder) + self.assertEqual( + self._tm._calculate_exception_length(), 12 + ) + + self._tm.client.framer = ModbusAsciiFramer(self.decoder) + self.assertEqual( + self._tm._calculate_exception_length(), 14 + ) + + def testCheckResponse(self): + mock_adu_size = 10 + self._tm.base_adu_size = mock_adu_size + self._tm.client = MagicMock() + + # case1: returns true: + mock_response = "somethinglegal" + self._tm.client.framer = ModbusSocketFramer + self.assertTrue(self._tm._check_response(mock_response)) + + def testExecute(self): + class MockRequest(object): + def __init__(self, tid=0, pdu=1024): + self.transaction_id = tid + self.pdu_size = pdu + + def get_response_pdu_size(self): + return self.pdu_size + + mock_request = MockRequest() + self._tm.client = MagicMock() + self._tm.base_adu_size = 10 + self._tm.send = MagicMock() + self._tm._recv = MagicMock(return_value="asd") + self._tm.getTransaction = MagicMock(return_value="mock_resp") + self._tm.retries = 1 + + # Test normal behaviour + self.assertEqual(self._tm.execute(mock_request), "mock_resp") + + # no transaction + self._tm.getTransaction = MagicMock(return_value=None) + self._tm.transactions = "" + mock_exception = 'No Response received from the remote unit' + self.assertIsInstance(self._tm.execute(mock_request), ModbusIOException) + + def testRecv(self): + self._tm.retries = 1 + self._tm.client = MagicMock() + self._tm.client._recv = MagicMock(return_value="asd") + self.assertIsNotNone(self._tm._recv(4)) + + self._tm.client.framer = ModbusSocketFramer(self.decoder) + self.assertIsNotNone(self._tm._recv(4)) + + self._tm._calculate_exception_length = MagicMock(return_value=0) + self._tm._check_response = MagicMock(retutn_value=True) + + def testAddTransaction(self): + self.assertRaises( + NotImplementedException, lambda: self._tm.addTransaction("asd") + ) + self.assertRaises( + NotImplementedException, lambda: self._tm.getTransaction("asd") + ) + self.assertRaises( + NotImplementedException, lambda: self._tm.delTransaction("asd") + ) + + def testGetNextTID(self): + mock_tid = 10 + self._tm.tid = mock_tid + self.assertEqual(self._tm.getNextTID(), mock_tid + 1) + + def testReset(self): + self._tm.tid = 100 + self._tm.transactions = MagicMock() + self._tm.reset() + self.assertEqual(self._tm.tid, 0) #---------------------------------------------------------------------------# # Dictionary based transaction manager #---------------------------------------------------------------------------# @@ -175,7 +293,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 +375,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 +401,42 @@ 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() + + #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 +496,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 +568,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 #---------------------------------------------------------------------------# From 3c9358526a0fcc33b24c4b72325f3e0a475e7d46 Mon Sep 17 00:00:00 2001 From: rahul Date: Mon, 4 Dec 2017 10:47:22 +0530 Subject: [PATCH 04/15] Removed ranom files --- pymodbus.db | Bin 12288 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pymodbus.db diff --git a/pymodbus.db b/pymodbus.db deleted file mode 100644 index a1ecc8efdf352a7065b45fcc7753be0dfa5426e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI#!AiqG5C-7gC~5;GJ%~L8hMW>8k>U#&!=i-}X_|TsDP5tMnragv~lz z=G8{8Mi`4A5tIaXV?nw~uGuN;wWOyY4j+Qp9eM*YJI?ZTAwL}&MTxib-)nIKp78_<6X&-AX1vwmoE*7n@I<;|Hs&8o6!^HhIHI@DL{ zTz{cFyK3sG2>}5JKmY;|fB*y_009U<00Izzz_ALLYGXM6AM5YMz90Yr2tWV=5P$## NAOHafKmY=*z#qktM-%`6 From f1a5ea8270ac7c3a31932c2b1edd0f6a4f475082 Mon Sep 17 00:00:00 2001 From: rahul Date: Mon, 4 Dec 2017 11:50:49 +0530 Subject: [PATCH 05/15] #245: Python3 compatibility --- pymodbus/transaction.py | 4 +--- test/test_server_async.py | 14 ++++++++------ test/test_transaction.py | 3 ++- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 2b8101c3d..9dd90be3c 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -132,10 +132,8 @@ def execute(self, request): if not result and self.retry_on_empty: retries -= 1 continue - if _logger.isEnabledFor(logging.DEBUG): - _test_msg = " ".join([hex(byte2int(x)) for x in result]) - _logger.debug("recv: {}".format(_test_msg)) + _logger.debug("recv: " + " ".join([hex(byte2int(x)) for x in result])) self.client.framer.processIncomingPacket(result, self.addTransaction) break except (socket.error, ModbusIOException, InvalidMessageRecievedException) as msg: diff --git a/test/test_server_async.py b/test/test_server_async.py index 6e36098bf..79c315cae 100644 --- a/test/test_server_async.py +++ b/test/test_server_async.py @@ -74,7 +74,8 @@ def testConnectionLost(self): def testDataReceived(self): protocol = ModbusTcpProtocol() - mock_data = "Hellow world!" + # 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])] @@ -129,13 +130,14 @@ 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="helloworld") + mock_data = MockMsg(resp=True, msg=mock_msg) protocol.control = MagicMock() protocol.framer = MagicMock() protocol.factory = MagicMock() - protocol.framer.buildPacket = MagicMock(return_value='a') + protocol.framer.buildPacket = MagicMock(return_value=mock_msg) protocol.transport= MagicMock() protocol._send(mock_data) @@ -184,7 +186,7 @@ def testSerialServerStartup(self): self.assertEqual(mock_reactor.run.call_count, 1) def testDatagramReceived(self): - mock_data = "hello world" + mock_data = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12\x34" mock_addr = 0x01 protocol = ModbusUdpProtocol(store=None) protocol.framer.processIncomingPacket = MagicMock() @@ -196,12 +198,12 @@ def testDatagramReceived(self): def testSendUdp(self): protocol = ModbusUdpProtocol(store=None) - mock_data = "hello world" + 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='a') + protocol.framer.buildPacket = MagicMock(return_value=mock_data) protocol.transport= MagicMock() protocol._send(mock_data, mock_addr) diff --git a/test/test_transaction.py b/test/test_transaction.py index 41a600a8d..b252c722b 100644 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -92,6 +92,7 @@ def testCheckResponse(self): self.assertTrue(self._tm._check_response(mock_response)) def testExecute(self): + mock_recv = b"\x00\x01\x12\x34\x00\x04\xff\x02\x12\x34" class MockRequest(object): def __init__(self, tid=0, pdu=1024): self.transaction_id = tid @@ -104,7 +105,7 @@ def get_response_pdu_size(self): self._tm.client = MagicMock() self._tm.base_adu_size = 10 self._tm.send = MagicMock() - self._tm._recv = MagicMock(return_value="asd") + self._tm._recv = MagicMock(return_value=mock_recv) self._tm.getTransaction = MagicMock(return_value="mock_resp") self._tm.retries = 1 From 0aa5718884e118b45c96de41cd7a8e5127847772 Mon Sep 17 00:00:00 2001 From: rahul Date: Mon, 4 Dec 2017 11:52:48 +0530 Subject: [PATCH 06/15] Added DB files to gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3e5d76b47..cb69d463e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ pymodbus.egg-info/ .vscode .idea .noseids - +*.db .idea/ .tox/ doc/api/epydoc/html/ From d6681b0593a3237019bd3f30a51ed708ba614111 Mon Sep 17 00:00:00 2001 From: rahul Date: Mon, 4 Dec 2017 12:03:46 +0530 Subject: [PATCH 07/15] Small changes - addign mock varaibles --- test/test_transaction.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/test_transaction.py b/test/test_transaction.py index b252c722b..0ec052228 100644 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -131,14 +131,15 @@ def testRecv(self): self._tm._check_response = MagicMock(retutn_value=True) def testAddTransaction(self): + mock_trans = "some trans" self.assertRaises( - NotImplementedException, lambda: self._tm.addTransaction("asd") + NotImplementedException, lambda: self._tm.addTransaction(mock_trans) ) self.assertRaises( - NotImplementedException, lambda: self._tm.getTransaction("asd") + NotImplementedException, lambda: self._tm.getTransaction(mock_trans) ) self.assertRaises( - NotImplementedException, lambda: self._tm.delTransaction("asd") + NotImplementedException, lambda: self._tm.delTransaction(mock_trans) ) def testGetNextTID(self): From 5b1f2dbccacf020cbde54e0173ad004c22af6d86 Mon Sep 17 00:00:00 2001 From: rahul Date: Mon, 4 Dec 2017 14:27:23 +0530 Subject: [PATCH 08/15] Testinf for python3.5 --- test/test_server_async.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/test_server_async.py b/test/test_server_async.py index 79c315cae..4b008c3cc 100644 --- a/test/test_server_async.py +++ b/test/test_server_async.py @@ -82,7 +82,9 @@ def testDataReceived(self): protocol.framer = protocol._execute = MagicMock() protocol.dataReceived(mock_data) - protocol.framer.processIncomingPacket.assert_called() + # import pdb; pdb.set_trace() + # protocol.framer.processIncomingPacket.assert_called() + self.assertTrue(protocol.framer.processIncomingPacket.called) # test datareceived returns None protocol.factory.control.ListenOnly = False @@ -142,7 +144,8 @@ def __init__(self, msg, resp=False): protocol._send(mock_data) - protocol.framer.buildPacket.assert_called_with(mock_data) + # protocol.framer.buildPacket.assert_called_with(mock_data) + self.assertTrue(protocol.framer.buildPacket.called) protocol.transport.write.assert_called() mock_data =MockMsg(resp=False, msg="helloworld") @@ -194,7 +197,8 @@ def testDatagramReceived(self): protocol._execute = MagicMock() protocol.datagramReceived(mock_data, mock_addr) - protocol.framer.processIncomingPacket.assert_called() + # protocol.framer.processIncomingPacket.assert_called() + self.assertTrue(protocol.framer.processIncomingPacket.called) def testSendUdp(self): protocol = ModbusUdpProtocol(store=None) @@ -208,7 +212,8 @@ def testSendUdp(self): protocol._send(mock_data, mock_addr) - protocol.framer.buildPacket.assert_called_with(mock_data) + # protocol.framer.buildPacket.assert_called_with(mock_data) + self.assertTrue(protocol.framer.buildPacket.called) protocol.transport.write.assert_called() def testUdpExecuteSuccess(self): From 80607862e450064fbe0698b6df7006c799f5d9ba Mon Sep 17 00:00:00 2001 From: rahul Date: Mon, 4 Dec 2017 14:34:22 +0530 Subject: [PATCH 09/15] Testinf for python3.5 --- test/test_server_async.py | 22 +++++++++++++++------- test/test_transaction.py | 1 + 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/test/test_server_async.py b/test/test_server_async.py index 4b008c3cc..e84d9963e 100644 --- a/test/test_server_async.py +++ b/test/test_server_async.py @@ -98,7 +98,8 @@ def testTcpExecuteSuccess(self): # tst if _send being called protocol._execute(request) - protocol._send.assert_called() + # protocol._send.assert_called() + self.assertTrue(protocol._send.called) def testTcpExecuteFailure(self): protocol = ModbusTcpProtocol() @@ -111,7 +112,8 @@ def testTcpExecuteFailure(self): self.assertRaises( NoSuchSlaveException, protocol._execute(request) ) - request.doException.assert_called() + # request.doException.assert_called() + self.assertTrue(request.doException.called) # CASE-2: NoSuchSlaveException with ignore_missing_slaves = true protocol.ignore_missing_slaves = True @@ -146,7 +148,8 @@ def __init__(self, msg, resp=False): # protocol.framer.buildPacket.assert_called_with(mock_data) self.assertTrue(protocol.framer.buildPacket.called) - protocol.transport.write.assert_called() + # protocol.transport.write.assert_called() + self.assertTrue(protocol.transport.write.called) mock_data =MockMsg(resp=False, msg="helloworld") self.assertEqual(protocol._send(mock_data), None) @@ -214,7 +217,9 @@ def testSendUdp(self): # protocol.framer.buildPacket.assert_called_with(mock_data) self.assertTrue(protocol.framer.buildPacket.called) - protocol.transport.write.assert_called() + # protocol.transport.write.assert_called() + self.assertTrue(protocol.transport.write.called) + def testUdpExecuteSuccess(self): protocol = ModbusUdpProtocol(store=None) @@ -225,7 +230,8 @@ def testUdpExecuteSuccess(self): # tst if _send being called protocol._execute(request, mock_addr) - protocol._send.assert_called() + # protocol._send.assert_called() + self.assertTrue(protocol._send.called) def testUdpExecuteFailure(self): protocol = ModbusUdpProtocol(store=None) @@ -239,7 +245,8 @@ def testUdpExecuteFailure(self): self.assertRaises( NoSuchSlaveException, protocol._execute(request, mock_addr) ) - request.doException.assert_called() + # request.doException.assert_called() + self.assertTrue(request.doException.called) # CASE-2: NoSuchSlaveException with ignore_missing_slaves = true protocol.ignore_missing_slaves = True @@ -258,7 +265,8 @@ def testStopServer(self): reactor.stop = MagicMock() StopServer() - reactor.stop.assert_called() + # reactor.stop.assert_called() + self.assertTrue(reactor.stop.called) def testIsMainThread(self): import threading diff --git a/test/test_transaction.py b/test/test_transaction.py index 0ec052228..7e63875e0 100644 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -421,6 +421,7 @@ def mock_callback(self): 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) From c1812f656111ed9f8f64ea2f3230415846f33cc4 Mon Sep 17 00:00:00 2001 From: rahul Date: Mon, 4 Dec 2017 14:38:25 +0530 Subject: [PATCH 10/15] Testinf for python3.5 --- test/test_server_async.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_server_async.py b/test/test_server_async.py index e84d9963e..f240993fa 100644 --- a/test/test_server_async.py +++ b/test/test_server_async.py @@ -125,7 +125,8 @@ def testTcpExecuteFailure(self): self.assertRaises( ModbusIOException, protocol._execute(request) ) - protocol._send.assert_called() + # protocol._send.assert_called() + self.assertTrue(protocol._send.called) def testSendTcp(self): @@ -258,7 +259,8 @@ def testUdpExecuteFailure(self): self.assertRaises( ModbusIOException, protocol._execute(request, mock_addr) ) - protocol._send.assert_called() + # protocol._send.assert_called() + self.assertTrue(protocol._send.called) def testStopServer(self): from twisted.internet import reactor From a8505755f6aa756d1b7acac20c4dfdcb2a352dee Mon Sep 17 00:00:00 2001 From: rahul Date: Mon, 4 Dec 2017 14:42:10 +0530 Subject: [PATCH 11/15] Testing for python3.5: all test pass --- test/test_server_async.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/test/test_server_async.py b/test/test_server_async.py index f240993fa..8fbd9758f 100644 --- a/test/test_server_async.py +++ b/test/test_server_async.py @@ -82,8 +82,6 @@ def testDataReceived(self): protocol.framer = protocol._execute = MagicMock() protocol.dataReceived(mock_data) - # import pdb; pdb.set_trace() - # protocol.framer.processIncomingPacket.assert_called() self.assertTrue(protocol.framer.processIncomingPacket.called) # test datareceived returns None @@ -98,7 +96,6 @@ def testTcpExecuteSuccess(self): # tst if _send being called protocol._execute(request) - # protocol._send.assert_called() self.assertTrue(protocol._send.called) def testTcpExecuteFailure(self): @@ -112,7 +109,6 @@ def testTcpExecuteFailure(self): self.assertRaises( NoSuchSlaveException, protocol._execute(request) ) - # request.doException.assert_called() self.assertTrue(request.doException.called) # CASE-2: NoSuchSlaveException with ignore_missing_slaves = true @@ -125,7 +121,6 @@ def testTcpExecuteFailure(self): self.assertRaises( ModbusIOException, protocol._execute(request) ) - # protocol._send.assert_called() self.assertTrue(protocol._send.called) def testSendTcp(self): @@ -147,9 +142,7 @@ def __init__(self, msg, resp=False): protocol._send(mock_data) - # protocol.framer.buildPacket.assert_called_with(mock_data) self.assertTrue(protocol.framer.buildPacket.called) - # protocol.transport.write.assert_called() self.assertTrue(protocol.transport.write.called) mock_data =MockMsg(resp=False, msg="helloworld") @@ -201,7 +194,6 @@ def testDatagramReceived(self): protocol._execute = MagicMock() protocol.datagramReceived(mock_data, mock_addr) - # protocol.framer.processIncomingPacket.assert_called() self.assertTrue(protocol.framer.processIncomingPacket.called) def testSendUdp(self): @@ -216,9 +208,7 @@ def testSendUdp(self): protocol._send(mock_data, mock_addr) - # protocol.framer.buildPacket.assert_called_with(mock_data) self.assertTrue(protocol.framer.buildPacket.called) - # protocol.transport.write.assert_called() self.assertTrue(protocol.transport.write.called) @@ -231,7 +221,6 @@ def testUdpExecuteSuccess(self): # tst if _send being called protocol._execute(request, mock_addr) - # protocol._send.assert_called() self.assertTrue(protocol._send.called) def testUdpExecuteFailure(self): @@ -246,7 +235,6 @@ def testUdpExecuteFailure(self): self.assertRaises( NoSuchSlaveException, protocol._execute(request, mock_addr) ) - # request.doException.assert_called() self.assertTrue(request.doException.called) # CASE-2: NoSuchSlaveException with ignore_missing_slaves = true @@ -259,7 +247,6 @@ def testUdpExecuteFailure(self): self.assertRaises( ModbusIOException, protocol._execute(request, mock_addr) ) - # protocol._send.assert_called() self.assertTrue(protocol._send.called) def testStopServer(self): @@ -267,7 +254,6 @@ def testStopServer(self): reactor.stop = MagicMock() StopServer() - # reactor.stop.assert_called() self.assertTrue(reactor.stop.called) def testIsMainThread(self): From 5cdfcc2bcc2476edc9dfedf98bf6686db248d4b3 Mon Sep 17 00:00:00 2001 From: rahul Date: Mon, 4 Dec 2017 20:02:10 +0530 Subject: [PATCH 12/15] Cleanup test code --- examples/contrib/message-generator.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/contrib/message-generator.py b/examples/contrib/message-generator.py index 9b6df7f45..f2b37f642 100755 --- a/examples/contrib/message-generator.py +++ b/examples/contrib/message-generator.py @@ -32,14 +32,12 @@ from pymodbus.register_read_message import * from pymodbus.register_write_message import * from pymodbus.compat import IS_PYTHON3 -from pymodbus.pdu import ExceptionResponse #--------------------------------------------------------------------------# # initialize logging #--------------------------------------------------------------------------# import logging modbus_log = logging.getLogger("pymodbus") -exp = ExceptionResponse(0x01, 0x0B) #--------------------------------------------------------------------------# # enumerate all request messages #--------------------------------------------------------------------------# @@ -82,8 +80,7 @@ ReturnSlaveBusCharacterOverrunCountRequest, ReturnIopOverrunCountRequest, ClearOverrunCountRequest, - GetClearModbusPlusRequest, - exp + GetClearModbusPlusRequest ] @@ -129,8 +126,7 @@ ReturnSlaveBusCharacterOverrunCountResponse, ReturnIopOverrunCountResponse, ClearOverrunCountResponse, - GetClearModbusPlusResponse, - exp + GetClearModbusPlusResponse ] From 004eca2caee5300d31e1cf4c090fa1d787d8c47b Mon Sep 17 00:00:00 2001 From: rahul Date: Fri, 22 Dec 2017 12:33:18 +0530 Subject: [PATCH 13/15] Merge --- examples/contrib/message_parser.py | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/examples/contrib/message_parser.py b/examples/contrib/message_parser.py index e38ea6907..bd440fd55 100755 --- a/examples/contrib/message_parser.py +++ b/examples/contrib/message_parser.py @@ -10,13 +10,8 @@ * ascii * rtu * binary -<<<<<<< HEAD:examples/contrib/message-parser.py -''' -#---------------------------------------------------------------------------# -======= """ # -------------------------------------------------------------------------- # ->>>>>>> dev:examples/contrib/message_parser.py # import needed libraries # -------------------------------------------------------------------------- # from __future__ import print_function @@ -25,20 +20,12 @@ from optparse import OptionParser import codecs as c -<<<<<<< HEAD:examples/contrib/message-parser.py -from pymodbus.utilities import computeCRC, computeLRC -======= ->>>>>>> dev:examples/contrib/message_parser.py from pymodbus.factory import ClientDecoder, ServerDecoder from pymodbus.transaction import ModbusSocketFramer from pymodbus.transaction import ModbusBinaryFramer from pymodbus.transaction import ModbusAsciiFramer from pymodbus.transaction import ModbusRtuFramer -<<<<<<< HEAD:examples/contrib/message-parser.py -from pymodbus.compat import byte2int, int2byte, IS_PYTHON3 -======= from pymodbus.compat import IS_PYTHON3 ->>>>>>> dev:examples/contrib/message_parser.py # -------------------------------------------------------------------------- # @@ -48,15 +35,9 @@ modbus_log = logging.getLogger("pymodbus") -<<<<<<< HEAD:examples/contrib/message-parser.py -#---------------------------------------------------------------------------# -# build a quick wrapper around the framers -#---------------------------------------------------------------------------# -======= # -------------------------------------------------------------------------- # # build a quick wrapper around the framers # -------------------------------------------------------------------------- # ->>>>>>> dev:examples/contrib/message_parser.py class Decoder(object): def __init__(self, framer, encode=False): @@ -72,11 +53,7 @@ def decode(self, message): """ Attempt to decode the supplied message :param message: The messge to decode -<<<<<<< HEAD:examples/contrib/message-parser.py - ''' -======= """ ->>>>>>> dev:examples/contrib/message_parser.py if IS_PYTHON3: value = message if self.encode else c.encode(message, 'hex_codec') else: @@ -130,15 +107,9 @@ def report(self, message): print("%-15s = %s" % ('documentation', message.__doc__)) -<<<<<<< HEAD:examples/contrib/message-parser.py -#---------------------------------------------------------------------------# -# and decode our message -#---------------------------------------------------------------------------# -======= # -------------------------------------------------------------------------- # # and decode our message # -------------------------------------------------------------------------- # ->>>>>>> dev:examples/contrib/message_parser.py def get_options(): """ A helper method to parse the command line options From a09667a064853b09e9e4a645b59687ce7ce53821 Mon Sep 17 00:00:00 2001 From: rahul Date: Fri, 22 Dec 2017 12:43:43 +0530 Subject: [PATCH 14/15] #245: Fixed tests --- pymodbus/transaction.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index ba0e69d64..24756f19f 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -168,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 @@ -936,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 #-----------------------------------------------------------------------# From 7d322b07f2661e8123e9aa57a23dcd6a6de3948d Mon Sep 17 00:00:00 2001 From: rahul Date: Fri, 22 Dec 2017 13:52:53 +0530 Subject: [PATCH 15/15] Removing copy of example/common/dbstore-update-server.py --- examples/common/dbstore-update-server.py | 94 ------------------------ 1 file changed, 94 deletions(-) delete mode 100644 examples/common/dbstore-update-server.py diff --git a/examples/common/dbstore-update-server.py b/examples/common/dbstore-update-server.py deleted file mode 100644 index b0ce87bf0..000000000 --- a/examples/common/dbstore-update-server.py +++ /dev/null @@ -1,94 +0,0 @@ -''' -Pymodbus Server With Updating Thread --------------------------------------------------------------------------- -This is an example of having a background thread updating the -context in an SQLite4 database while the server is operating. - -This scrit generates a random address range (within 0 - 65000) and a random -value and stores it in a database. It then reads the same address to verify -that the process works as expected - -This can also be done with a python thread:: - from threading import Thread - thread = Thread(target=updating_writer, args=(context,)) - thread.start() -''' -#---------------------------------------------------------------------------# -# import the modbus libraries we need -#---------------------------------------------------------------------------# -from pymodbus.server.async import StartTcpServer -from pymodbus.device import ModbusDeviceIdentification -from pymodbus.datastore import ModbusSequentialDataBlock -from pymodbus.datastore import ModbusServerContext -from pymodbus.datastore.database import SqlSlaveContext -from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer -import random - -#---------------------------------------------------------------------------# -# import the twisted libraries we need -#---------------------------------------------------------------------------# -from twisted.internet.task import LoopingCall - -#---------------------------------------------------------------------------# -# configure the service logging -#---------------------------------------------------------------------------# -import logging -logging.basicConfig() -log = logging.getLogger() -log.setLevel(logging.DEBUG) - -#---------------------------------------------------------------------------# -# define your callback process -#---------------------------------------------------------------------------# -def updating_writer(a): - ''' A worker process that runs every so often and - updates live values of the context which resides in an SQLite3 database. - It should be noted that there is a race condition for the update. - :param arguments: The input arguments to the call - ''' - log.debug("Updating the database context") - context = a[0] - readfunction = 0x03 # read holding registers - writefunction = 0x10 - slave_id = 0x01 # slave address - count = 50 - - # import pdb; pdb.set_trace() - - rand_value = random.randint(0, 9999) - rand_addr = random.randint(0, 65000) - log.debug("Writing to datastore: {}, {}".format(rand_addr, rand_value)) - # import pdb; pdb.set_trace() - context[slave_id].setValues(writefunction, rand_addr, [rand_value]) - values = context[slave_id].getValues(readfunction, rand_addr, count) - log.debug("Values from datastore: " + str(values)) - - - -#---------------------------------------------------------------------------# -# initialize your data store -#---------------------------------------------------------------------------# -block = ModbusSequentialDataBlock(0x00, [0]*0xff) -store = SqlSlaveContext(block) - -context = ModbusServerContext(slaves={1: store}, single=False) - - -#---------------------------------------------------------------------------# -# initialize the server information -#---------------------------------------------------------------------------# -identity = ModbusDeviceIdentification() -identity.VendorName = 'pymodbus' -identity.ProductCode = 'PM' -identity.VendorUrl = 'http://github.com/bashwork/pymodbus/' -identity.ProductName = 'pymodbus Server' -identity.ModelName = 'pymodbus Server' -identity.MajorMinorRevision = '1.0' - -#---------------------------------------------------------------------------# -# run the server you want -#---------------------------------------------------------------------------# -time = 5 # 5 seconds delay -loop = LoopingCall(f=updating_writer, a=(context,)) -loop.start(time, now=False) # initially delay by time -StartTcpServer(context, identity=identity, address=("", 5020))