xref: /aosp_15_r20/external/pigweed/pw_transfer/integration_test/expected_errors_test.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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