1# Copyright 2024 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Cross-language pw_transfer tests that verify failure modes. 15 16Note: these tests DO NOT work with older integration test binaries that only 17support the legacy transfer protocol. 18 19Usage: 20 21 bazel run pw_transfer/integration_test:expected_errors_test 22 23Command-line arguments must be provided after a double-dash: 24 25 bazel run pw_transfer/integration_test:expected_errors_test -- \ 26 --server-port 3304 27 28Which tests to run can be specified as command-line arguments: 29 30 bazel run pw_transfer/integration_test:expected_errors_test -- \ 31 ErrorTransferIntegrationTest.test_write_to_unknown_id 32 33""" 34 35import asyncio 36from parameterized import parameterized 37import random 38import tempfile 39 40from google.protobuf import text_format 41 42from pw_transfer.integration_test import config_pb2 43from pw_transfer.integration_test import test_fixture 44from pw_protobuf_protos import status_pb2 45from test_fixture import TransferIntegrationTestHarness, TransferConfig 46 47 48class ErrorTransferIntegrationTest(test_fixture.TransferIntegrationTest): 49 # Each set of transfer tests uses a different client/server port pair to 50 # allow tests to be run in parallel. 51 HARNESS_CONFIG = TransferIntegrationTestHarness.Config( 52 server_port=3312, client_port=3313 53 ) 54 55 @parameterized.expand( 56 [ 57 ("cpp"), 58 ("java"), 59 ("python"), 60 ] 61 ) 62 def test_write_to_unknown_id(self, client_type): 63 payload = b"Rabbits are the best pets" 64 config = self.default_config() 65 resource_id = 5 66 67 with tempfile.NamedTemporaryFile() as f_payload, tempfile.NamedTemporaryFile() as f_server_output: 68 # Add the resource at a different resource ID. 69 config.server.resources[resource_id + 1].destination_paths.append( 70 f_server_output.name 71 ) 72 config.client.transfer_actions.append( 73 config_pb2.TransferAction( 74 resource_id=resource_id, 75 file_path=f_payload.name, 76 transfer_type=config_pb2.TransferAction.TransferType.WRITE_TO_SERVER, 77 expected_status=status_pb2.StatusCode.NOT_FOUND, 78 ) 79 ) 80 81 f_payload.write(payload) 82 f_payload.flush() # Ensure contents are there to read! 83 exit_codes = asyncio.run( 84 self.harness.perform_transfers( 85 config.server, client_type, config.client, config.proxy 86 ) 87 ) 88 89 self.assertEqual(exit_codes.client, 0) 90 self.assertEqual(exit_codes.server, 0) 91 92 @parameterized.expand( 93 [ 94 ("cpp"), 95 ("java"), 96 ("python"), 97 ] 98 ) 99 def test_client_write_timeout(self, client_type): 100 payload = random.Random(67336391945).randbytes(4321) 101 config = TransferConfig( 102 self.default_server_config(), 103 self.default_client_config(), 104 text_format.Parse( 105 """ 106 client_filter_stack: [ 107 { hdlc_packetizer: {} }, 108 { 109 server_failure: { 110 packets_before_failure: [5, 5], 111 start_immediately: true, 112 only_consider_transfer_chunks: true, 113 } 114 } 115 ] 116 117 server_filter_stack: [ 118 { hdlc_packetizer: {} } 119 ]""", 120 config_pb2.ProxyConfig(), 121 ), 122 ) 123 resource_id = 987654321 124 125 # This test deliberately tries to time out the transfer, so because of 126 # the retry process the resource ID might be re-initialized multiple 127 # times. 128 self.do_single_write( 129 client_type, 130 config, 131 resource_id, 132 payload, 133 permanent_resource_id=True, 134 expected_status=status_pb2.StatusCode.DEADLINE_EXCEEDED, 135 ) 136 137 @parameterized.expand( 138 [ 139 ("cpp"), 140 ("java"), 141 ("python"), 142 ] 143 ) 144 def test_server_write_timeout(self, client_type): 145 payload = random.Random(67336391945).randbytes(4321) 146 147 # Set pending_bytes to a low value so the server sends multiple 148 # parameters chunks for the proxy to drop. 149 server_config = self.default_server_config() 150 server_config.pending_bytes = 1024 151 152 config = TransferConfig( 153 server_config, 154 self.default_client_config(), 155 text_format.Parse( 156 """ 157 client_filter_stack: [ 158 { hdlc_packetizer: {} } 159 ] 160 161 server_filter_stack: [ 162 { hdlc_packetizer: {} }, 163 { 164 server_failure: { 165 packets_before_failure: [3, 3], 166 only_consider_transfer_chunks: true, 167 } 168 } 169 ]""", 170 config_pb2.ProxyConfig(), 171 ), 172 ) 173 resource_id = 987654321 174 175 # This test deliberately tries to time out the transfer, so because of 176 # the retry process the resource ID might be re-initialized multiple 177 # times. 178 self.do_single_write( 179 client_type, 180 config, 181 resource_id, 182 payload, 183 permanent_resource_id=True, 184 expected_status=status_pb2.StatusCode.DEADLINE_EXCEEDED, 185 ) 186 187 @parameterized.expand( 188 [ 189 ("cpp"), 190 ("java"), 191 ("python"), 192 ] 193 ) 194 def test_read_from_unknown_id(self, client_type): 195 payload = b"Rabbits are the best pets" 196 config = self.default_config() 197 resource_id = 5 198 199 with tempfile.NamedTemporaryFile() as f_payload, tempfile.NamedTemporaryFile() as f_client_output: 200 # Add the resource at a different resource ID. 201 config.server.resources[resource_id + 1].source_paths.append( 202 f_payload.name 203 ) 204 config.client.transfer_actions.append( 205 config_pb2.TransferAction( 206 resource_id=resource_id, 207 file_path=f_client_output.name, 208 transfer_type=config_pb2.TransferAction.TransferType.READ_FROM_SERVER, 209 expected_status=status_pb2.StatusCode.NOT_FOUND, 210 ) 211 ) 212 213 f_payload.write(payload) 214 f_payload.flush() # Ensure contents are there to read! 215 exit_codes = asyncio.run( 216 self.harness.perform_transfers( 217 config.server, client_type, config.client, config.proxy 218 ) 219 ) 220 221 self.assertEqual(exit_codes.client, 0) 222 self.assertEqual(exit_codes.server, 0) 223 224 @parameterized.expand( 225 [ 226 ("cpp"), 227 ("java"), 228 ("python"), 229 ] 230 ) 231 def test_client_read_timeout(self, client_type): 232 # This must be > 8192 in order to exceed the window_end default and 233 # cause a timeout on python client 234 payload = random.Random(67336391945).randbytes(10321) 235 236 config = TransferConfig( 237 self.default_server_config(), 238 self.default_client_config(), 239 text_format.Parse( 240 """ 241 client_filter_stack: [ 242 { hdlc_packetizer: {} }, 243 { 244 server_failure: { 245 packets_before_failure: [2, 2], 246 start_immediately: true, 247 only_consider_transfer_chunks: true, 248 } 249 } 250 ] 251 252 server_filter_stack: [ 253 { hdlc_packetizer: {} } 254 ]""", 255 config_pb2.ProxyConfig(), 256 ), 257 ) 258 resource_id = 987654321 259 260 # This test deliberately tries to time out the transfer, so because of 261 # the retry process the resource ID might be re-initialized multiple 262 # times. 263 self.do_single_read( 264 client_type, 265 config, 266 resource_id, 267 payload, 268 permanent_resource_id=True, 269 expected_status=status_pb2.StatusCode.DEADLINE_EXCEEDED, 270 ) 271 272 @parameterized.expand( 273 [ 274 ("cpp"), 275 ("java"), 276 ("python"), 277 ] 278 ) 279 def test_server_read_timeout(self, client_type): 280 payload = random.Random(67336391945).randbytes(4321) 281 config = TransferConfig( 282 self.default_server_config(), 283 self.default_client_config(), 284 text_format.Parse( 285 """ 286 client_filter_stack: [ 287 { hdlc_packetizer: {} } 288 ] 289 290 server_filter_stack: [ 291 { hdlc_packetizer: {} }, 292 { 293 server_failure: { 294 packets_before_failure: [5, 5], 295 only_consider_transfer_chunks: true, 296 } 297 } 298 ]""", 299 config_pb2.ProxyConfig(), 300 ), 301 ) 302 resource_id = 987654321 303 304 # This test deliberately tries to time out the transfer, so because of 305 # the retry process the resource ID might be re-initialized multiple 306 # times. 307 self.do_single_read( 308 client_type, 309 config, 310 resource_id, 311 payload, 312 permanent_resource_id=True, 313 expected_status=status_pb2.StatusCode.DEADLINE_EXCEEDED, 314 ) 315 316 @parameterized.expand( 317 [ 318 ("cpp"), 319 ("java"), 320 ("python"), 321 ] 322 ) 323 def test_data_drop_client_lifetime_timeout(self, client_type): 324 """Drops the first data chunk of a transfer but allows the rest.""" 325 payload = random.Random(67336391945).randbytes(1234) 326 327 # This test is expected to hit the lifetime retry count, so make it 328 # reasonable. 329 client_config = self.default_client_config() 330 client_config.max_lifetime_retries = 20 331 client_config.chunk_timeout_ms = 1000 332 333 config = TransferConfig( 334 self.default_server_config(), 335 client_config, 336 text_format.Parse( 337 """ 338 client_filter_stack: [ 339 { hdlc_packetizer: {} }, 340 { window_packet_dropper: { window_packet_to_drop: 0 } } 341 ] 342 343 server_filter_stack: [ 344 { hdlc_packetizer: {} }, 345 { window_packet_dropper: { window_packet_to_drop: 0 } } 346 ]""", 347 config_pb2.ProxyConfig(), 348 ), 349 ) 350 # Resource ID is arbitrary, but deliberately set to be >1 byte. 351 resource_id = 7332 352 353 # This test deliberately tries to time out the transfer, so because of 354 # the retry process the resource ID might be re-initialized multiple 355 # times. 356 self.do_single_read( 357 client_type, 358 config, 359 resource_id, 360 payload, 361 permanent_resource_id=True, 362 expected_status=status_pb2.StatusCode.DEADLINE_EXCEEDED, 363 ) 364 365 @parameterized.expand( 366 [ 367 ("cpp"), 368 ("java"), 369 ("python"), 370 ] 371 ) 372 def test_offset_read_unimpl_handler(self, client_type): 373 payload = b"Rabbits are the best pets" 374 config = self.default_config() 375 resource_id = 5 376 377 config = self.default_config() 378 379 self.do_single_read( 380 client_type, 381 config, 382 resource_id, 383 payload, 384 initial_offset=len(payload), 385 expected_status=status_pb2.StatusCode.UNIMPLEMENTED, 386 ) 387 388 @parameterized.expand( 389 [ 390 ("cpp"), 391 ("java"), 392 ("python"), 393 ] 394 ) 395 def test_offset_write_unimpl_handler(self, client_type): 396 payload = b"Rabbits are the best pets" 397 config = self.default_config() 398 resource_id = 5 399 400 config = self.default_config() 401 402 self.do_single_write( 403 client_type, 404 config, 405 resource_id, 406 payload, 407 initial_offset=len(payload), 408 expected_status=status_pb2.StatusCode.UNIMPLEMENTED, 409 ) 410 411 @parameterized.expand( 412 [ 413 ("cpp"), 414 ("java"), 415 ("python"), 416 ] 417 ) 418 def test_offset_read_invalid_offset(self, client_type): 419 payload = b"Rabbits are the best pets" 420 config = self.default_config() 421 resource_id = 6 422 423 config = self.default_config() 424 425 self.do_single_read( 426 client_type, 427 config, 428 resource_id, 429 payload, 430 initial_offset=len(payload) + 1, 431 offsettable_resources=True, 432 expected_status=status_pb2.StatusCode.RESOURCE_EXHAUSTED, 433 ) 434 435 436if __name__ == '__main__': 437 test_fixture.run_tests_for(ErrorTransferIntegrationTest) 438