diff --git a/doc/index.rst b/doc/index.rst index c2073f1f9..92678510f 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -12,11 +12,12 @@ Please select a topic in the left hand column. source/client source/server source/repl + source/simulator3 source/simulator source/examples source/authors source/changelog source/internals source/roadmap - -.. include:: ../README.rst \ No newline at end of file + +.. include:: ../README.rst diff --git a/doc/source/_static/examples.tgz b/doc/source/_static/examples.tgz index 20f2ef99b..bb0b4bc12 100644 Binary files a/doc/source/_static/examples.tgz and b/doc/source/_static/examples.tgz differ diff --git a/doc/source/_static/examples.zip b/doc/source/_static/examples.zip index fbbb7a4bf..8f1a501a0 100644 Binary files a/doc/source/_static/examples.zip and b/doc/source/_static/examples.zip differ diff --git a/doc/source/library/simulator/config.rst b/doc/source/library/simulator/config.rst index ea89c262f..918ba3049 100644 --- a/doc/source/library/simulator/config.rst +++ b/doc/source/library/simulator/config.rst @@ -25,7 +25,7 @@ each containing a list of servers/devices } You can define as many server and devices as you like, when starting -:ref:`pymodbus.simulator` you select one server and one device to simulate. +:ref:`pymodbus.simulator (v3.x)` you select one server and one device to simulate. A entry in “device_list” correspond to the dict you can use as parameter to datastore_simulator is you want to construct your own simulator. diff --git a/doc/source/library/simulator/web.rst b/doc/source/library/simulator/web.rst index b4c2cc252..47c5e916b 100644 --- a/doc/source/library/simulator/web.rst +++ b/doc/source/library/simulator/web.rst @@ -1,12 +1,12 @@ -Web frontend -============ +Web frontend v3.x +================= TO BE DOCUMENTED. -pymodbus.simulator ------------------- +pymodbus.simulator (v3.x) +------------------------- The easiest way to run the simulator with web is to use "pymodbus.simulator" from the commandline. diff --git a/doc/source/simulator.rst b/doc/source/simulator.rst index 3ebe94ebd..da262e9cb 100644 --- a/doc/source/simulator.rst +++ b/doc/source/simulator.rst @@ -1,40 +1,104 @@ Simulator ========= -The simulator is a full fledged modbus simulator, which is -constantly being evolved with user ideas / amendments. +The simulator is a full fledged modbus server/simulator. The purpose of the simulator is to provide support for client application test harnesses with end-to-end testing simulating real life modbus devices. -The datastore simulator allows the user to (all automated) +The simulator allows the user to (all automated): - simulate a modbus device by adding a simple configuration, -- test how a client handles modbus exceptions, +- simulate a multipoint line, but adding multiple device configurations, +- simulate devices that are not conforming to the protocol, +- simulate communication problems (data loss etc), +- test how a client handles modbus response and exceptions, - test a client apps correct use of the simulated device. -The web interface allows the user to (online / manual) +The web interface (activated optionally) allows the user to: -- test how a client handles modbus errors, -- test how a client handles communication errors like divided messages, -- run your test server in the cloud, +- introduce modbus errors (like e.g. wrong length), +- introduce communication errors (like splitting a message), - monitor requests/responses, -- inject modbus errors like malicious a response, - see/Change values online. +- inject modbus errors like malicious a response, +- run your test server in the cloud, The REST API allow the test process to be automated -- spin up a test server with unix domain sockets in your test harness, +- spin up a test server in your test harness, - set expected responses with a simple REST API command, -- check the result with another simple REST API command, +- check the result with a simple REST API command, - test your client app in a true end-to-end fashion. -.. toctree:: - :maxdepth: 4 - :hidden: +The web server uses the REST API internally, which helps to ensure that it +actually works. + + +Data model configuration +------------------------ + +.. warning:: from v3.9.0 this is available as a "normal" datastore model. + +The simulator data model represent the registers and parameters of the simulated devices. +The data model is defined using :class:`SimData` and :class:`SimDevice` before starting the +server and cannot be changed without restarting the server. + +:class:`SimData` defines a group of continuous identical registers. This is the basis of the model, +multiple :class:`SimData` should be used to mirror the physical device. + +:class:`SimDevice` defines device parameters and a list of :class:`SimData`. +The list of :class:`SimData` can added as shared registers or as the 4 blocks, defined in modbus. +:class:`SimDevice` can be used to simulate a single device, while a list of +:class:`SimDevice` simulates a multipoint line (simulating a rs485 line or a tcp based serial forwarder). + +A server consist of communication parameters and a device or a list of devices + +:class:`SimDataType` is a helper class that defines legal datatypes. + +:class:`SimActions` is a helper class that defines built in actions. + +:github:`examples/simulator_datamodel.py` contains usage examples. + +SimData +^^^^^^^ + +.. autoclass:: pymodbus.simulator.SimData + :members: + :undoc-members: + :show-inheritance: + +SimDevice +^^^^^^^^^ + +.. autoclass:: pymodbus.simulator.SimDevice + :members: + :undoc-members: + :show-inheritance: + +SimDataType +^^^^^^^^^^^ + +.. autoclass:: pymodbus.simulator.SimDataType + :members: + :undoc-members: + :show-inheritance: + + +Simulator server +---------------- + +.. note:: This is a v4.0.0 functionality currently not available, please see the 3x simulator server. + + +Web frontend +------------ + +.. note:: This is a v4.0.0 functionality currently not available, please see the 3x simulator server. + + +REST API +-------- - library/simulator/config - library/simulator/datastore - library/simulator/web - library/simulator/restapi +.. note:: This is a v4.0.0 functionality currently not available, please see the 3x simulator server. diff --git a/doc/source/simulator3.rst b/doc/source/simulator3.rst new file mode 100644 index 000000000..78f2f5084 --- /dev/null +++ b/doc/source/simulator3.rst @@ -0,0 +1,42 @@ +Simulator (3.x) +=============== + +.. warning:: Beginning with v3.9.0 and ending with v4.0.0 this simulator will be replaced by a new version. + +The simulator is a full fledged modbus simulator, which is +constantly being evolved with user ideas / amendments. + +The purpose of the simulator is to provide support for client +application test harnesses with end-to-end testing simulating real life +modbus devices. + +The datastore simulator allows the user to (all automated) + +- simulate a modbus device by adding a simple configuration, +- test how a client handles modbus exceptions, +- test a client apps correct use of the simulated device. + +The web interface allows the user to (online / manual) + +- test how a client handles modbus errors, +- test how a client handles communication errors like divided messages, +- run your test server in the cloud, +- monitor requests/responses, +- inject modbus errors like malicious a response, +- see/Change values online. + +The REST API allow the test process to be automated + +- spin up a test server with unix domain sockets in your test harness, +- set expected responses with a simple REST API command, +- check the result with another simple REST API command, +- test your client app in a true end-to-end fashion. + +.. toctree:: + :maxdepth: 4 + :hidden: + + library/simulator/config + library/simulator/datastore + library/simulator/web + library/simulator/restapi diff --git a/examples/simulator_datamodel.py b/examples/simulator_datamodel.py new file mode 100755 index 000000000..0751d961c --- /dev/null +++ b/examples/simulator_datamodel.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Pymodbus simulator datamodel examples. + +This example shows how to configure the simulator datamodel to mimic a real +device. + +There are different examples, to show the flexibility of the simulator datamodel. + +.. tip:: This is NOT the pymodbus simulator, that is started as pymodbus.simulator. +""" + +from pymodbus.simulator import SimCheckConfig, SimData, SimDataType, SimDevice + + +def define_registers(): + """Define simulator data model. + + Coils and direct inputs are expressed as bits representing a relay in the device. + There are no real difference between coils and direct inputs, but historically + they have been divided. + + Holding registers and input registers are the same, but historically they have + been divided. + + Coils and direct inputs are handled differently in shared vs non-shared models. + + - In a non-shared model the address is the bit directly. It can be thought of as if a + register only contains 1 bit. + - In a shared model the address is the register containing the bits. So a single bit CANNOT + be addressed directly. + """ + # Define a group of coils (remark difference between shared and non-shared) + block_coil = [SimData(0, count=100, datatype=SimDataType.DEFAULT), + SimData(0, True, 16)] + block_coil_shared = [SimData(0, 0xFFFF, 16)] + + # SimData can be reused with copying + block_direct = block_coil + + # Define a group of registers (remark NO difference between shared and non-shared) + block_holding = [SimData(10, count=100, datatype=SimDataType.DEFAULT), + SimData(10, 123.4, datatype=SimDataType.FLOAT32), + SimData(12, 123456789.3, datatype=SimDataType.FLOAT64), + SimData(17, value=123, count=5, datatype=SimDataType.INT32), + SimData(27, "Hello ", datatype=SimDataType.STRING)] + block_input = block_holding + block_shared = [SimData(10, 123.4, datatype=SimDataType.FLOAT32), + SimData(12, 123456789.3, datatype=SimDataType.FLOAT64), + SimData(16, 0xf0f0, datatype=SimDataType.BITS), + SimData(17, value=123, count=5, datatype=SimDataType.INT32), + SimData(27, "Hello ", datatype=SimDataType.STRING)] + + device_block = SimDevice(1, False, + block_coil=block_coil, + block_direct=block_direct, + block_holding=block_holding, + block_input=block_input) + device_shared = SimDevice(2, False, + block_shared=block_coil_shared+block_shared) + assert not SimCheckConfig([device_block]) + assert not SimCheckConfig([device_shared]) + assert not SimCheckConfig([device_shared, device_block]) + +def main(): + """Combine setup and run.""" + define_registers() + +if __name__ == "__main__": + main() diff --git a/pymodbus/server/startstop.py b/pymodbus/server/startstop.py index 6578e2f28..ea5615d59 100644 --- a/pymodbus/server/startstop.py +++ b/pymodbus/server/startstop.py @@ -16,7 +16,7 @@ ) -async def StartAsyncTcpServer( # pylint: disable=invalid-name +async def StartAsyncTcpServer( context: ModbusServerContext, custom_functions: list[type[ModbusPDU]] | None = None, **kwargs, @@ -35,7 +35,7 @@ async def StartAsyncTcpServer( # pylint: disable=invalid-name await ModbusTcpServer(context, custom_pdu=custom_functions, **kwargs).serve_forever() -def StartTcpServer( # pylint: disable=invalid-name +def StartTcpServer( context: ModbusServerContext, custom_functions: list[type[ModbusPDU]] | None = None, **kwargs @@ -54,7 +54,7 @@ def StartTcpServer( # pylint: disable=invalid-name asyncio.run(StartAsyncTcpServer(context, custom_functions=custom_functions, **kwargs)) -async def StartAsyncTlsServer( # pylint: disable=invalid-name +async def StartAsyncTlsServer( context: ModbusServerContext, custom_functions: list[type[ModbusPDU]] | None = None, **kwargs, @@ -73,7 +73,7 @@ async def StartAsyncTlsServer( # pylint: disable=invalid-name await ModbusTlsServer(context, custom_pdu=custom_functions, **kwargs).serve_forever() -def StartTlsServer( # pylint: disable=invalid-name +def StartTlsServer( context: ModbusServerContext, custom_functions: list[type[ModbusPDU]] | None = None, **kwargs @@ -92,7 +92,7 @@ def StartTlsServer( # pylint: disable=invalid-name asyncio.run(StartAsyncTlsServer(context, custom_functions=custom_functions, **kwargs)) -async def StartAsyncUdpServer( # pylint: disable=invalid-name +async def StartAsyncUdpServer( context: ModbusServerContext, custom_functions: list[type[ModbusPDU]] | None = None, **kwargs, @@ -111,7 +111,7 @@ async def StartAsyncUdpServer( # pylint: disable=invalid-name await ModbusUdpServer(context, custom_pdu=custom_functions, **kwargs).serve_forever() -def StartUdpServer( # pylint: disable=invalid-name +def StartUdpServer( context: ModbusServerContext, custom_functions: list[type[ModbusPDU]] | None = None, **kwargs @@ -130,7 +130,7 @@ def StartUdpServer( # pylint: disable=invalid-name asyncio.run(StartAsyncUdpServer(context, custom_functions=custom_functions, **kwargs)) -async def StartAsyncSerialServer( # pylint: disable=invalid-name +async def StartAsyncSerialServer( context: ModbusServerContext, custom_functions: list[type[ModbusPDU]] | None = None, **kwargs, @@ -149,7 +149,7 @@ async def StartAsyncSerialServer( # pylint: disable=invalid-name await ModbusSerialServer(context, custom_pdu=custom_functions, **kwargs).serve_forever() -def StartSerialServer( # pylint: disable=invalid-name +def StartSerialServer( context: ModbusServerContext, custom_functions: list[type[ModbusPDU]] | None = None, **kwargs @@ -168,7 +168,7 @@ def StartSerialServer( # pylint: disable=invalid-name asyncio.run(StartAsyncSerialServer(context, custom_functions=custom_functions, **kwargs)) -async def ServerAsyncStop() -> None: # pylint: disable=invalid-name +async def ServerAsyncStop() -> None: """Terminate server.""" if not ModbusBaseServer.active_server: raise RuntimeError("Modbus server not running.") @@ -176,7 +176,7 @@ async def ServerAsyncStop() -> None: # pylint: disable=invalid-name ModbusBaseServer.active_server = None -def ServerStop() -> None: # pylint: disable=invalid-name +def ServerStop() -> None: """Terminate server.""" if not ModbusBaseServer.active_server: raise RuntimeError("Modbus server not running.") diff --git a/pymodbus/simulator/__init__.py b/pymodbus/simulator/__init__.py new file mode 100644 index 000000000..629aa3d54 --- /dev/null +++ b/pymodbus/simulator/__init__.py @@ -0,0 +1,10 @@ +"""Simulator.""" + +__all__ = [ + "SimCheckConfig", + "SimData", + "SimDataType", + "SimDevice" +] + +from pymodbus.simulator.simdata import SimCheckConfig, SimData, SimDataType, SimDevice diff --git a/pymodbus/simulator/simcore.py b/pymodbus/simulator/simcore.py new file mode 100644 index 000000000..7ec5ef57a --- /dev/null +++ b/pymodbus/simulator/simcore.py @@ -0,0 +1,6 @@ +"""Simulator data model classes.""" +from __future__ import annotations + + +class SimCore: # pylint: disable=too-few-public-methods + """Datastore for the simulator/server.""" diff --git a/pymodbus/simulator/simdata.py b/pymodbus/simulator/simdata.py new file mode 100644 index 000000000..f5d181ecb --- /dev/null +++ b/pymodbus/simulator/simdata.py @@ -0,0 +1,255 @@ +"""Simulator data model classes.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from enum import Enum + + +class SimDataType(Enum): + """Register types, used to define group of registers. + + This is the types pymodbus recognizes, actually the modbus standard do NOT define e.g. INT32, + but since nearly every device have e.g. INT32 as part of its register map, it was decided to + include it in pymodbus, with automatic conversions to/from registers. + """ + + #: 1 integer == 1 register + INT16 = 1 + #: 1 positive integer == 1 register + UINT16 = 2 + #: 1 integer == 2 registers + INT32 = 3 + #: 1 positive integer == 2 register2 + UINT32 = 4 + #: 1 integer == 4 registers + INT64 = 5 + #: 1 positive integer == 4 register + UINT64 = 6 + #: 1 float == 2 registers + FLOAT32 = 7 + #: 1 float == 4 registers + FLOAT64 = 8 + #: 1 string == len(string) / 2 registers + #: + #: .. tip:: String length must be a multiple of 2 (corresponding to registers). + STRING = 9 + #: Shared mode: 16 bits == 1 register else 1 bit == 1 "register" (address) + BITS = 10 + #: Raw registers + #: + #: .. warning:: Do not use as default, since it fills the memory and block other registrations. + REGISTERS = 11 + #: Raw registers, but also sets register address limits. + #: + #: .. tip:: It a single but special register, and therefore improves speed and memory usage compared to REGISTERS. + DEFAULT = 12 + +@dataclass(frozen=True) +class SimData: + """Configure a group of continuous identical registers. + + **Example**: + + .. code-block:: python + + SimData( + start_register=100, + count=5, + value=-123456 + datatype=SimDataType.INT32 + ) + + The above code defines 5 INT32, each with the value -123456, in total 20 registers. + + .. tip:: use SimDatatype.DEFAULT to define register limits: + + .. code-block:: python + + SimData( + start_register=0, # First legal registers + count=1000, # last legal register is start_register+count-1 + value=0x1234 # Default register value + datatype=SimDataType.DEFAULT + ) + + The above code sets the range of legal registers to 0..9999 all with the value 0x1234. + Accessing non-defined registers will cause an exception response. + + .. attention:: Using SimDataType.DEFAULT is a LOT more efficient to define all registers, than \ + the other datatypes. This is because default registers are not created unless written to, whereas \ + the registers of other datatypes are each created as objects. + """ + + #: Address of first register, starting with 0. + #: + #: .. caution:: No default, must be defined. + start_register: int + + #: Value of datatype, to initialize the registers (repeated with count, apart from string). + #: + #: Depending on in which block the object is used some value types are not legal e.g. float cannot + #: be used to define coils. + value: int | float | str | bool | bytes = 0 + + #: Count of datatype e.g. count=3 datatype=SimdataType.INT32 is 6 registers. + #: + #: SimdataType.STR is special: + #: + #: - count=1, value="ABCD" is 2 registers + #: - count=3, value="ABCD" is 6 registers, with "ABCD" repeated 3 times. + count: int = 1 + + #: Datatype, used to check access and calculate register count. + #: + #: .. note:: Default is SimDataType.REGISTERS + datatype: SimDataType = SimDataType.REGISTERS + + #: Optional function to call when registers are being read/written. + #: + #: **Example function:** + #: + #: .. code-block:: python + #: + #: def my_action( + #: addr: int, + #: value: int | float | str | bool | bytes + #: ) -> int | float | str | bool | bytes: + #: return value + 1 + #: + #: .. tip:: use functools.partial to add extra parameters if needed. + action: Callable[[int, int | float | str | bool | bytes], int | float | str | bool | bytes] | None = None + + def __post_init__(self): + """Define a group of registers.""" + if not isinstance(self.start_register, int) or not 0 <= self.start_register < 65535: + raise TypeError("0 <= start_register < 65535") + if not isinstance(self.count, int) or not 0 < self.count <= 65535: + raise TypeError("0 < count <= 65535") + if not isinstance(self.datatype, SimDataType): + raise TypeError("datatype not SimDataType") + if self.action and not callable(self.action): + raise TypeError("action not Callable") + + +@dataclass(frozen=True) +class SimDevice: + """Configure a device with parameters and registers. + + Registers can be defined as shared or as 4 separate blocks. + + shared_block means all requests access the same registers, + allowing e.g. coils to be read as a holding register (except if type_checking is True). + + .. warning:: Shared mode cannot be mixed with non-shared mode ! + + In shared mode, individual coils/direct input cannot be addressed directly ! Instead + the register address is used with count. In non-shared mode coils/direct input can be + addressed directly. + + **Device with shared registers**:: + + SimDevice( + id=0, + block_shared=[SimData(...)] + ) + + **Device with non-shared registers**:: + + SimDevice( + id=0, + block_coil=[SimData(...)], + block_direct=[SimData(...)], + block_holding=[SimData(...)], + block_input=[SimData(...)], + ) + + A server can contain either a single :class:`SimDevice` or list of :class:`SimDevice` to simulate a + multipoint line. + """ + + #: Address of device + #: + #: Default 0 means accept all devices, except those defined in the same server. + #: + #: .. warning:: A server with a single device id=0 accept all requests. + id: int = 0 + + #: Enforce type checking, if True access are controlled to be conform with datatypes. + #: + #: Used to control that read_coils do not access a register defined as holding and visaversa + type_check: bool = False + + #: Use this block for shared registers (Modern devices). + #: + #: Requests accesses all registers in this block. + #: + #: .. warning:: cannot be used together with other block_* parameters! + block_shared: list[SimData] | None = None + + #: Use this block for non-shared registers (very old devices). + #: + #: In this block an address is a single coil, there are no registers. + #: + #: Request of type read/write_coil accesses this block. + #: + #: .. tip:: block_coil/direct/holding/input must all be defined + block_coil: list[SimData] | None = None + + #: Use this block for non-shared registers (very old devices). + #: + #: In this block an address is a single direct relay, there are no registers. + #: + #: Request of type read/write_direct_input accesses this block. + #: + #: .. tip:: block_coil/direct/holding/input must all be defined + block_direct: list[SimData] | None = None + + #: Use this block for non-shared registers (very old devices). + #: + #: In this block an address is a register. + #: + #: Request of type read/write_holding accesses this block. + #: + #: .. tip:: block_coil/direct/holding/input must all be defined + block_holding: list[SimData] | None = None + + + #: Use this block for non-shared registers (very old devices). + #: + #: In this block an address is a register. + #: + #: Request of type read/write_input accesses this block. + #: + #: .. tip:: block_coil/direct/holding/input must all be defined + block_input: list[SimData] | None = None + + def __post_init__(self): + """Define a device.""" + if not isinstance(self.id, int) or not 0 <= self.id < 255: + raise TypeError("0 <= id < 255") + blocks = [(self.block_shared, "shared")] + if self.block_shared: + if self.block_coil or self.block_direct or self.block_holding or self.block_input: + raise TypeError("block_* cannot be used with block_shared") + else: + blocks = [ + (self.block_coil, "coil"), + (self.block_direct, "direct"), + (self.block_holding, "holding"), + (self.block_input, "input")] + + for block, name in blocks: + if not block: + raise TypeError(f"block_{name} not defined") + if not isinstance(block, list): + raise TypeError(f"block_{name} not a list") + for entry in block: + if not isinstance(entry, SimData): + raise TypeError(f"block_{name} contains non SimData entries") + + +def SimCheckConfig(devices: list[SimDevice]) -> bool: + """Verify configuration.""" + _ = devices + return False diff --git a/pyproject.toml b/pyproject.toml index d1f25dd44..dc15d32c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,8 +160,9 @@ max-module-lines = "2000" [tool.pylint.basic] good-names = "i,j,k,rr,fc,rq,fd,x,_" -attr-rgx = "([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$" +attr-rgx = "([A-Za-z_][A-Za-z0-9_]{1,30}|(__.*__))$" method-rgx = "[a-z_][a-zA-Z0-9_]{2,}$" +function-rgx = "[A-Za-z_][A-Za-z0-9_]{2,}$" [tool.pylint.design] max-positional = 15 diff --git a/test/examples/__init__.py b/test/examples/__init__.py deleted file mode 100644 index 5f2eb76e0..000000000 --- a/test/examples/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Test of examples.""" diff --git a/test/examples/test_examples.py b/test/examples/test_examples.py index dac91e480..6e3d950e5 100755 --- a/test/examples/test_examples.py +++ b/test/examples/test_examples.py @@ -17,7 +17,7 @@ from examples.client_calls import template_call from examples.client_payload import main as main_payload_calls from examples.custom_msg import main as main_custom_client -from examples.datastore_simulator_share import main as main_datastore_simulator_share +from examples.datastore_simulator_share import main as main_datastore_simulator_share3 from examples.message_parser import main as main_parse_messages from examples.server_async import setup_server from examples.server_callback import run_callback_server @@ -26,7 +26,8 @@ from examples.server_updating import main as main_updating_server from examples.simple_async_client import run_async_simple_client from examples.simple_sync_client import run_sync_simple_client -from examples.simulator import run_simulator +from examples.simulator import run_simulator as run_simulator3 +from examples.simulator_datamodel import main as run_main_simulator_datamodel from pymodbus.exceptions import ModbusException from pymodbus.pdu import ExceptionResponse from pymodbus.server import ServerAsyncStop, ServerStop @@ -63,7 +64,7 @@ async def test_server_callback(self, use_port, use_host): await task async def test_updating_server(self, use_port, use_host): - """Test server simulator.""" + """Test server server updating.""" cmdargs = ["--port", str(use_port), "--host", use_host] task = asyncio.create_task(main_updating_server(cmdline=cmdargs)) task.set_name("run main_updating_server") @@ -79,8 +80,8 @@ async def test_updating_server(self, use_port, use_host): async def test_datastore_simulator_share(self, use_port, use_host): """Test server simulator.""" cmdargs = ["--port", str(use_port), "--host", use_host] - task = asyncio.create_task(main_datastore_simulator_share(cmdline=cmdargs)) - task.set_name("run main_datastore_simulator") + task = asyncio.create_task(main_datastore_simulator_share3(cmdline=cmdargs)) + task.set_name("run main_datastore_simulator3") await asyncio.sleep(0.1) testclient = setup_async_client(cmdline=cmdargs) await run_async_client(testclient, modbus_calls=run_a_few_calls) @@ -90,10 +91,14 @@ async def test_datastore_simulator_share(self, use_port, use_host): task.cancel() await task - async def test_simulator(self): - """Run simulator server/client.""" + async def test_simulator3(self): + """Run simulator3 server/client.""" # Awaiting fix, missing stop of task. - await run_simulator() + await run_simulator3() + + def test_simulator_datamodel(self): + """Run different simulator configurations.""" + run_main_simulator_datamodel() async def test_modbus_forwarder(self): """Test modbus forwarder.""" diff --git a/test/framer/__init__.py b/test/framer/__init__.py deleted file mode 100644 index 6120f1a8f..000000000 --- a/test/framer/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Test of message layer.""" diff --git a/test/framer/test_framer.py b/test/framer/test_framer.py index a07ceed03..15ea4ce60 100644 --- a/test/framer/test_framer.py +++ b/test/framer/test_framer.py @@ -12,8 +12,7 @@ FramerType, ) from pymodbus.pdu import DecodePDU, ModbusPDU - -from .generator import set_calls +from test.framer.generator import set_calls class TestFramer: diff --git a/test/pdu/__init__.py b/test/pdu/__init__.py deleted file mode 100644 index 6120f1a8f..000000000 --- a/test/pdu/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Test of message layer.""" diff --git a/test/pdu/test_register_write_messages.py b/test/pdu/test_register_write_messages.py index 239abbfe2..1dd4cf08d 100644 --- a/test/pdu/test_register_write_messages.py +++ b/test/pdu/test_register_write_messages.py @@ -9,8 +9,7 @@ WriteSingleRegisterRequest, WriteSingleRegisterResponse, ) - -from ..conftest import MockLastValuesContext +from test.conftest import MockLastValuesContext # ---------------------------------------------------------------------------# diff --git a/test/simulator/test_simdata.py b/test/simulator/test_simdata.py new file mode 100644 index 000000000..9791d928d --- /dev/null +++ b/test/simulator/test_simdata.py @@ -0,0 +1,66 @@ +"""Test pdu.""" + +import pytest + +from pymodbus.simulator import SimData, SimDataType, SimDevice + + +class TestSimData: + """Test simulator data config.""" + + def test_instanciate(self): + """Test that simdata can be objects.""" + a = SimData(0) + SimDevice(0, block_shared=[a]) + + @pytest.mark.parametrize("start_register", ["not ok", 1.0, -1, 70000]) + def test_simdata_start_register(self, start_register): + """Test that simdata can be objects.""" + with pytest.raises(TypeError): + SimData(start_register=start_register) + SimData(0) + + @pytest.mark.parametrize("count", ["not ok", 1.0, -1, 70000]) + def test_simdata_count(self, count): + """Test that simdata can be objects.""" + with pytest.raises(TypeError): + SimData(start_register=0, count=count) + SimData(start_register=0, count=2) + + @pytest.mark.parametrize("datatype", ["not ok", 1.0, 11]) + def test_simdata_datatype(self, datatype): + """Test that simdata can be objects.""" + with pytest.raises(TypeError): + SimData(start_register=0, datatype=datatype) + SimData(start_register=0, datatype=SimDataType.BITS) + + @pytest.mark.parametrize("action", ["my action"]) + def test_simdata_action(self, action): + """Test that simdata can be objects.""" + def dummy_action(): + """Set action.""" + + with pytest.raises(TypeError): + SimData(start_register=0, action=action) + SimData(start_register=0, action=dummy_action) + + @pytest.mark.parametrize("id", ["not ok", 1.0, 256]) + def test_simid(self, id): + """Test that simdata can be objects.""" + with pytest.raises(TypeError): + SimDevice(id=id) + SimDevice(id=1, block_shared=[SimData(0)]) + + def test_block_shared(self): + """Test that simdata can be objects.""" + with pytest.raises(TypeError): + SimDevice(id=1, block_shared=[SimData(0)], block_coil=[SimData(0)]) + with pytest.raises(TypeError): + SimDevice(id=1, block_coil=[SimData(0)]) + + def test_wrong_block(self): + """Test that simdata can be objects.""" + with pytest.raises(TypeError): + SimDevice(id=1, block_shared=SimData(0)) + with pytest.raises(TypeError): + SimDevice(id=1, block_coil=["no valid"]) diff --git a/test/transport/__init__.py b/test/transport/__init__.py deleted file mode 100644 index 430da4624..000000000 --- a/test/transport/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Test of transport layer."""