Skip to content

Commit c027dc8

Browse files
authored
Add support for server testing in package_test_tool. (#2044)
1 parent ae266cf commit c027dc8

File tree

3 files changed

+222
-132
lines changed

3 files changed

+222
-132
lines changed

examples/client_test_tool.py

Lines changed: 0 additions & 131 deletions
This file was deleted.

examples/package_test_tool.py

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
#!/usr/bin/env python3
2+
"""Pymodbus client testing tool.
3+
4+
usage::
5+
6+
package_test_tool.py
7+
8+
This is a tool to test how a client react to responses from a malicious server using:
9+
10+
ClientTester
11+
12+
and to how the server react to requests using:
13+
14+
ServerTester
15+
16+
The tool is intended for users with advanced modbus protocol knowledge.
17+
18+
When testing a client the server is replaced by a stub and the nullmodem solution.
19+
20+
There are 4 functions which can be modified to test the client/server functionality.
21+
22+
*** client_calls(client) ***
23+
24+
Called when the client is connected.
25+
26+
The full client API is available, just as if it was a normal App using pymodbus
27+
28+
*** server_calls(transport) ***
29+
30+
Called when the server is listening and stub connected.
31+
32+
Send raw data packets to the server (remark data is frame+request)
33+
34+
*** handle_client_data(transport, data) ***
35+
36+
Called when data is received from the client/server (remark data is frame+request)
37+
38+
The function generates frame+response and sends it.
39+
40+
And one function which can be modified to test the server functionality:
41+
"""
42+
from __future__ import annotations
43+
44+
import asyncio
45+
from typing import Callable
46+
47+
import pymodbus.client as modbusClient
48+
import pymodbus.server as modbusServer
49+
from pymodbus import Framer, pymodbus_apply_logging_config
50+
from pymodbus.datastore import (
51+
ModbusSequentialDataBlock,
52+
ModbusServerContext,
53+
ModbusSlaveContext,
54+
)
55+
from pymodbus.device import ModbusDeviceIdentification
56+
from pymodbus.logging import Log
57+
from pymodbus.transport import NULLMODEM_HOST, CommParams, CommType, ModbusProtocol
58+
59+
60+
class TransportStub(ModbusProtocol):
61+
"""Protocol layer including transport."""
62+
63+
def __init__(
64+
self,
65+
params: CommParams,
66+
is_server: bool,
67+
handler: Callable[[bytes], bytes],
68+
) -> None:
69+
"""Initialize a stub instance."""
70+
self.stub_handle_data = handler
71+
super().__init__(params, is_server)
72+
73+
async def start_run(self):
74+
"""Call need functions to start server/client."""
75+
if self.is_server:
76+
return await self.transport_listen()
77+
return await self.transport_connect()
78+
79+
def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
80+
"""Handle received data."""
81+
self.stub_handle_data(self, data)
82+
return len(data)
83+
84+
def callback_new_connection(self) -> ModbusProtocol:
85+
"""Call when listener receive new connection request."""
86+
new_stub = TransportStub(self.comm_params, False, self.stub_handle_data)
87+
new_stub.stub_handle_data = self.stub_handle_data
88+
return new_stub
89+
90+
91+
class ClientTester: # pylint: disable=too-few-public-methods
92+
"""Main program."""
93+
94+
def __init__(self, comm: CommType):
95+
"""Initialize runtime tester."""
96+
self.comm = comm
97+
98+
if comm == CommType.TCP:
99+
self.client = modbusClient.AsyncModbusTcpClient(
100+
NULLMODEM_HOST,
101+
port=5004,
102+
)
103+
elif comm == CommType.SERIAL:
104+
self.client = modbusClient.AsyncModbusSerialClient(
105+
f"{NULLMODEM_HOST}:5004",
106+
)
107+
else:
108+
raise RuntimeError("ERROR: CommType not implemented")
109+
server_params = self.client.comm_params.copy()
110+
server_params.source_address = (f"{NULLMODEM_HOST}:5004", 5004)
111+
self.stub = TransportStub(server_params, True, handle_client_data)
112+
113+
114+
async def run(self):
115+
"""Execute test run."""
116+
pymodbus_apply_logging_config()
117+
Log.debug("--> Start testing.")
118+
await self.stub.start_run()
119+
await self.client.connect()
120+
assert self.client.connected
121+
await client_calls(self.client)
122+
Log.debug("--> Closing.")
123+
self.client.close()
124+
125+
126+
class ServerTester: # pylint: disable=too-few-public-methods
127+
"""Main program."""
128+
129+
def __init__(self, comm: CommType):
130+
"""Initialize runtime tester."""
131+
self.comm = comm
132+
self.store = ModbusSlaveContext(
133+
di=ModbusSequentialDataBlock(0, [17] * 100),
134+
co=ModbusSequentialDataBlock(0, [17] * 100),
135+
hr=ModbusSequentialDataBlock(0, [17] * 100),
136+
ir=ModbusSequentialDataBlock(0, [17] * 100),
137+
)
138+
self.context = ModbusServerContext(slaves=self.store, single=True)
139+
self.identity = ModbusDeviceIdentification(
140+
info_name={"VendorName": "VendorName"}
141+
)
142+
if comm == CommType.TCP:
143+
self.server = modbusServer.ModbusTcpServer(
144+
self.context,
145+
framer=Framer.SOCKET,
146+
identity=self.identity,
147+
address=(NULLMODEM_HOST, 5004),
148+
)
149+
elif comm == CommType.SERIAL:
150+
self.server = modbusServer.ModbusSerialServer(
151+
self.context,
152+
framer=Framer.SOCKET,
153+
identity=self.identity,
154+
port=f"{NULLMODEM_HOST}:5004",
155+
)
156+
else:
157+
raise RuntimeError("ERROR: CommType not implemented")
158+
client_params = self.server.comm_params.copy()
159+
client_params.host = client_params.source_address[0]
160+
client_params.port = client_params.source_address[1]
161+
client_params.timeout_connect = 1.0
162+
self.stub = TransportStub(client_params, False, handle_server_data)
163+
164+
165+
async def run(self):
166+
"""Execute test run."""
167+
pymodbus_apply_logging_config()
168+
Log.debug("--> Start testing.")
169+
await self.server.transport_listen()
170+
await self.stub.start_run()
171+
await server_calls(self.stub)
172+
Log.debug("--> Shutting down.")
173+
await self.server.shutdown()
174+
175+
176+
async def main(comm: CommType, use_server: bool):
177+
"""Combine setup and run."""
178+
if use_server:
179+
test = ServerTester(comm)
180+
else:
181+
test = ClientTester(comm)
182+
await test.run()
183+
184+
185+
# -------------- USER CHANGES --------------
186+
187+
async def client_calls(client):
188+
"""Test client API."""
189+
Log.debug("--> Client calls starting.")
190+
_resp = await client.read_holding_registers(address=124, count=4, slave=0)
191+
192+
193+
async def server_calls(transport: ModbusProtocol):
194+
"""Test client API."""
195+
Log.debug("--> Client calls starting.")
196+
_resp = transport.transport_send(b'\x00\x01\x00\x00\x00\x06\x01\x03\x00\x00\x00\x01')
197+
await asyncio.sleep(1)
198+
print("--> JIX done")
199+
200+
201+
def handle_client_data(transport: ModbusProtocol, data: bytes):
202+
"""Respond to request at transport level."""
203+
Log.debug("--> stub called with request {}.", data, ":hex")
204+
response = b'\x01\x03\x08\x00\x05\x00\x05\x00\x00\x00\x00\x0c\xd7'
205+
206+
# Multiple send is allowed, to test fragmentation
207+
# for data in response:
208+
# to_send = data.to_bytes()
209+
# transport.transport_send(to_send)
210+
transport.transport_send(response)
211+
212+
213+
def handle_server_data(_transport: ModbusProtocol, data: bytes):
214+
"""Respond to request at transport level."""
215+
Log.debug("--> stub called with request {}.", data, ":hex")
216+
217+
218+
if __name__ == "__main__":
219+
# True for Server test, False for Client test
220+
# asyncio.run(main(CommType.SERIAL, False), debug=True)
221+
asyncio.run(main(CommType.TCP, True), debug=True)

pymodbus/framer/socket_framer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def checkFrame(self):
6868
elif len(self._buffer) - self._hsize + 1 >= self._header["len"]:
6969
return True
7070
# we don't have enough of a message yet, wait
71+
Log.debug("Frame check failed, missing part of message!!")
7172
return False
7273

7374
def advanceFrame(self):
@@ -131,7 +132,6 @@ def frameProcessIncomingPacket(self, single, callback, slave, tid=None, **kwargs
131132
"""
132133
while True:
133134
if not self.checkFrame():
134-
Log.debug("Frame check failed, ignoring!!")
135135
return
136136
if not self._validate_slave_id(slave, single):
137137
header_txt = self._header["uid"]

0 commit comments

Comments
 (0)