77
88from pymodbus .constants import ExcCodes
99from pymodbus .exceptions import ModbusIOException
10+ from pymodbus .pdu .decoders import DecodePDU
1011from pymodbus .pdu .register_message import (
1112 ReadHoldingRegistersRequest ,
1213 ReadHoldingRegistersResponse ,
@@ -127,6 +128,42 @@ def test_readwrite_decode_write_count_above_limit(self):
127128 request .x_decode (struct .pack (">HHHHB" , 1 , 1 , 1 , 122 , 244 ) + b"\x00 \x00 " * 122 )
128129 assert request .write_count == 122
129130
131+ def test_readwrite_decode_read_count_above_limit (self ):
132+ """Decoding must accept read_count above 125 so the server can answer 0x03."""
133+ request = ReadWriteMultipleRegistersRequest ()
134+ request .x_decode (struct .pack (">HHHHB" , 1 , 126 , 1 , 1 , 2 ) + b"\x00 \x00 " )
135+ assert request .read_count == 126
136+
137+ async def test_readwrite_read_count_above_limit_returns_illegal_value (
138+ self , mock_server_context
139+ ):
140+ """An out-of-range FC23 read count must answer 0x03."""
141+ context = mock_server_context ()
142+ context .async_getValues = mock .AsyncMock ()
143+ context .async_setValues = mock .AsyncMock ()
144+ request = ReadWriteMultipleRegistersRequest (
145+ read_address = 1 , read_count = 126 , write_address = 1 , write_registers = [5 ]
146+ )
147+ result = await request .datastore_update (context , 1 )
148+ assert result .function_code == 0x97
149+ assert result .exception_code == ExcCodes .ILLEGAL_VALUE
150+ context .async_getValues .assert_not_awaited ()
151+ context .async_setValues .assert_not_awaited ()
152+
153+ async def test_readwrite_read_count_at_max_returns_normal_response (
154+ self , mock_server_context
155+ ):
156+ """A read count exactly at MAX_READ_COUNT must be accepted."""
157+ context = mock_server_context ()
158+ request = ReadWriteMultipleRegistersRequest (
159+ read_address = 1 ,
160+ read_count = ReadWriteMultipleRegistersRequest .MAX_READ_COUNT ,
161+ write_address = 1 ,
162+ write_registers = [5 ],
163+ )
164+ result = await request .datastore_update (context , 1 )
165+ assert result .function_code == request .function_code
166+
130167 async def test_register_read_requests_count_errors (self , mock_server_context ):
131168 """This tests that the register request messages.
132169
@@ -147,6 +184,67 @@ async def test_register_read_requests_count_errors(self, mock_server_context):
147184 result = await request .datastore_update (context , 0 )
148185 assert result .exception_code == ExcCodes .ILLEGAL_VALUE
149186
187+ @pytest .mark .parametrize (
188+ ("request_class" , "function_code" ),
189+ [
190+ (ReadHoldingRegistersRequest , 3 ),
191+ (ReadInputRegistersRequest , 4 ),
192+ ],
193+ )
194+ async def test_read_register_count_above_max_returns_illegal_value (
195+ self , request_class , function_code , mock_server_context
196+ ):
197+ """An out-of-range read quantity must answer 0x03, not silence."""
198+ context = mock_server_context ()
199+ context .async_getValues = mock .AsyncMock (return_value = [1 ] * 10 )
200+ request = request_class (address = 0 , count = request_class .MAX_COUNT + 1 )
201+ result = await request .datastore_update (context , 1 )
202+ assert result .function_code == function_code | 0x80
203+ assert result .exception_code == ExcCodes .ILLEGAL_VALUE
204+ context .async_getValues .assert_not_awaited ()
205+
206+ @pytest .mark .parametrize (
207+ "request_class" ,
208+ [ReadHoldingRegistersRequest , ReadInputRegistersRequest ],
209+ )
210+ async def test_read_register_count_zero_returns_illegal_value (
211+ self , request_class , mock_server_context
212+ ):
213+ """A zero read quantity must answer 0x03."""
214+ context = mock_server_context ()
215+ context .async_getValues = mock .AsyncMock (return_value = [1 ] * 10 )
216+ result = await request_class (address = 0 , count = 0 ).datastore_update (context , 1 )
217+ assert result .function_code == request_class .function_code | 0x80
218+ assert result .exception_code == ExcCodes .ILLEGAL_VALUE
219+ context .async_getValues .assert_not_awaited ()
220+
221+ @pytest .mark .parametrize (
222+ "request_class" ,
223+ [ReadHoldingRegistersRequest , ReadInputRegistersRequest ],
224+ )
225+ async def test_read_register_count_at_max_returns_normal_response (
226+ self , request_class , mock_server_context
227+ ):
228+ """A count exactly at the maximum must be accepted, not rejected."""
229+ context = mock_server_context ()
230+ request = request_class (address = 0 , count = request_class .MAX_COUNT )
231+ result = await request .datastore_update (context , 1 )
232+ assert result .function_code == request_class .function_code
233+
234+ def test_read_register_decode_accepts_count_above_max (self ):
235+ """Decoding must not raise for an over-max read quantity.
236+
237+ Raising here makes DecodePDU.decode() swallow the frame and the server
238+ answer nothing, replacing a correct exception response with silence.
239+ """
240+ decoder = DecodePDU (True )
241+ for frame , expected_count in (
242+ (b"\x03 \x00 \x00 \x00 \x7e " , 126 ),
243+ (b"\x04 \x00 \x00 \x00 \x7e " , 126 ),
244+ ):
245+ pdu = decoder .decode (frame )
246+ assert pdu .count == expected_count
247+
150248 async def test_register_read_requests_verify_errors (self , mock_server_context ):
151249 """This tests that the register request messages.
152250
0 commit comments