xref: /curl/tests/http/test_02_download.py (revision bef0acaf)
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3#***************************************************************************
4#                                  _   _ ____  _
5#  Project                     ___| | | |  _ \| |
6#                             / __| | | | |_) | |
7#                            | (__| |_| |  _ <| |___
8#                             \___|\___/|_| \_\_____|
9#
10# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
11#
12# This software is licensed as described in the file COPYING, which
13# you should have received as part of this distribution. The terms
14# are also available at https://curl.se/docs/copyright.html.
15#
16# You may opt to use, copy, modify, merge, publish, distribute and/or sell
17# copies of the Software, and permit persons to whom the Software is
18# furnished to do so, under the terms of the COPYING file.
19#
20# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
21# KIND, either express or implied.
22#
23# SPDX-License-Identifier: curl
24#
25###########################################################################
26#
27import difflib
28import filecmp
29import logging
30import math
31import os
32from datetime import timedelta
33import pytest
34
35from testenv import Env, CurlClient, LocalClient
36
37
38log = logging.getLogger(__name__)
39
40
41class TestDownload:
42
43    @pytest.fixture(autouse=True, scope='class')
44    def _class_scope(self, env, httpd, nghttpx):
45        if env.have_h3():
46            nghttpx.start_if_needed()
47        httpd.clear_extra_configs()
48        httpd.reload()
49
50    @pytest.fixture(autouse=True, scope='class')
51    def _class_scope(self, env, httpd):
52        indir = httpd.docs_dir
53        env.make_data_file(indir=indir, fname="data-10k", fsize=10*1024)
54        env.make_data_file(indir=indir, fname="data-100k", fsize=100*1024)
55        env.make_data_file(indir=indir, fname="data-1m", fsize=1024*1024)
56        env.make_data_file(indir=indir, fname="data-10m", fsize=10*1024*1024)
57        env.make_data_file(indir=indir, fname="data-50m", fsize=50*1024*1024)
58
59    # download 1 file
60    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
61    def test_02_01_download_1(self, env: Env, httpd, nghttpx, repeat, proto):
62        if proto == 'h3' and not env.have_h3():
63            pytest.skip("h3 not supported")
64        curl = CurlClient(env=env)
65        url = f'https://{env.authority_for(env.domain1, proto)}/data.json'
66        r = curl.http_download(urls=[url], alpn_proto=proto)
67        r.check_response(http_status=200)
68
69    # download 2 files
70    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
71    def test_02_02_download_2(self, env: Env, httpd, nghttpx, repeat, proto):
72        if proto == 'h3' and not env.have_h3():
73            pytest.skip("h3 not supported")
74        curl = CurlClient(env=env)
75        url = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-1]'
76        r = curl.http_download(urls=[url], alpn_proto=proto)
77        r.check_response(http_status=200, count=2)
78
79    # download 100 files sequentially
80    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
81    def test_02_03_download_sequential(self, env: Env,
82                                       httpd, nghttpx, repeat, proto):
83        if proto == 'h3' and not env.have_h3():
84            pytest.skip("h3 not supported")
85        count = 10
86        curl = CurlClient(env=env)
87        urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'
88        r = curl.http_download(urls=[urln], alpn_proto=proto)
89        r.check_response(http_status=200, count=count, connect_count=1)
90
91    # download 100 files parallel
92    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
93    def test_02_04_download_parallel(self, env: Env,
94                                     httpd, nghttpx, repeat, proto):
95        if proto == 'h3' and not env.have_h3():
96            pytest.skip("h3 not supported")
97        count = 10
98        max_parallel = 5
99        curl = CurlClient(env=env)
100        urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'
101        r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
102            '--parallel', '--parallel-max', f'{max_parallel}'
103        ])
104        r.check_response(http_status=200, count=count)
105        if proto == 'http/1.1':
106            # http/1.1 parallel transfers will open multiple connections
107            assert r.total_connects > 1, r.dump_logs()
108        else:
109            # http2 parallel transfers will use one connection (common limit is 100)
110            assert r.total_connects == 1, r.dump_logs()
111
112    # download 500 files sequential
113    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
114    def test_02_05_download_many_sequential(self, env: Env,
115                                            httpd, nghttpx, repeat, proto):
116        if proto == 'h3' and not env.have_h3():
117            pytest.skip("h3 not supported")
118        if proto == 'h3' and env.curl_uses_lib('msh3'):
119            pytest.skip("msh3 shaky here")
120        count = 200
121        curl = CurlClient(env=env)
122        urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'
123        r = curl.http_download(urls=[urln], alpn_proto=proto)
124        r.check_response(http_status=200, count=count)
125        if proto == 'http/1.1':
126            # http/1.1 parallel transfers will open multiple connections
127            assert r.total_connects > 1, r.dump_logs()
128        else:
129            # http2 parallel transfers will use one connection (common limit is 100)
130            assert r.total_connects == 1, r.dump_logs()
131
132    # download 500 files parallel
133    @pytest.mark.parametrize("proto", ['h2', 'h3'])
134    def test_02_06_download_many_parallel(self, env: Env,
135                                          httpd, nghttpx, repeat, proto):
136        if proto == 'h3' and not env.have_h3():
137            pytest.skip("h3 not supported")
138        count = 200
139        max_parallel = 50
140        curl = CurlClient(env=env)
141        urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[000-{count-1}]'
142        r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
143            '--parallel', '--parallel-max', f'{max_parallel}'
144        ])
145        r.check_response(http_status=200, count=count, connect_count=1)
146
147    # download files parallel, check connection reuse/multiplex
148    @pytest.mark.parametrize("proto", ['h2', 'h3'])
149    def test_02_07_download_reuse(self, env: Env,
150                                  httpd, nghttpx, repeat, proto):
151        if proto == 'h3' and not env.have_h3():
152            pytest.skip("h3 not supported")
153        count = 200
154        curl = CurlClient(env=env)
155        urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'
156        r = curl.http_download(urls=[urln], alpn_proto=proto,
157                               with_stats=True, extra_args=[
158            '--parallel', '--parallel-max', '200'
159        ])
160        r.check_response(http_status=200, count=count)
161        # should have used at most 2 connections only (test servers allow 100 req/conn)
162        # it may be just 1 on slow systems where request are answered faster than
163        # curl can exhaust the capacity or if curl runs with address-sanitizer speed
164        assert r.total_connects <= 2, "h2 should use fewer connections here"
165
166    # download files parallel with http/1.1, check connection not reused
167    @pytest.mark.parametrize("proto", ['http/1.1'])
168    def test_02_07b_download_reuse(self, env: Env,
169                                   httpd, nghttpx, repeat, proto):
170        if env.curl_uses_lib('wolfssl'):
171            pytest.skip("wolfssl session reuse borked")
172        count = 6
173        curl = CurlClient(env=env)
174        urln = f'https://{env.authority_for(env.domain1, proto)}/data.json?[0-{count-1}]'
175        r = curl.http_download(urls=[urln], alpn_proto=proto,
176                               with_stats=True, extra_args=[
177            '--parallel'
178        ])
179        r.check_response(count=count, http_status=200)
180        # http/1.1 should have used count connections
181        assert r.total_connects == count, "http/1.1 should use this many connections"
182
183    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
184    def test_02_08_1MB_serial(self, env: Env,
185                              httpd, nghttpx, repeat, proto):
186        if proto == 'h3' and not env.have_h3():
187            pytest.skip("h3 not supported")
188        count = 5
189        urln = f'https://{env.authority_for(env.domain1, proto)}/data-1m?[0-{count-1}]'
190        curl = CurlClient(env=env)
191        r = curl.http_download(urls=[urln], alpn_proto=proto)
192        r.check_response(count=count, http_status=200)
193
194    @pytest.mark.parametrize("proto", ['h2', 'h3'])
195    def test_02_09_1MB_parallel(self, env: Env,
196                              httpd, nghttpx, repeat, proto):
197        if proto == 'h3' and not env.have_h3():
198            pytest.skip("h3 not supported")
199        count = 5
200        urln = f'https://{env.authority_for(env.domain1, proto)}/data-1m?[0-{count-1}]'
201        curl = CurlClient(env=env)
202        r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
203            '--parallel'
204        ])
205        r.check_response(count=count, http_status=200)
206
207    @pytest.mark.skipif(condition=Env().slow_network, reason="not suitable for slow network tests")
208    @pytest.mark.skipif(condition=Env().ci_run, reason="not suitable for CI runs")
209    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
210    def test_02_10_10MB_serial(self, env: Env,
211                              httpd, nghttpx, repeat, proto):
212        if proto == 'h3' and not env.have_h3():
213            pytest.skip("h3 not supported")
214        count = 3
215        urln = f'https://{env.authority_for(env.domain1, proto)}/data-10m?[0-{count-1}]'
216        curl = CurlClient(env=env)
217        r = curl.http_download(urls=[urln], alpn_proto=proto)
218        r.check_response(count=count, http_status=200)
219
220    @pytest.mark.skipif(condition=Env().slow_network, reason="not suitable for slow network tests")
221    @pytest.mark.skipif(condition=Env().ci_run, reason="not suitable for CI runs")
222    @pytest.mark.parametrize("proto", ['h2', 'h3'])
223    def test_02_11_10MB_parallel(self, env: Env,
224                              httpd, nghttpx, repeat, proto):
225        if proto == 'h3' and not env.have_h3():
226            pytest.skip("h3 not supported")
227        if proto == 'h3' and env.curl_uses_lib('msh3'):
228            pytest.skip("msh3 stalls here")
229        count = 3
230        urln = f'https://{env.authority_for(env.domain1, proto)}/data-10m?[0-{count-1}]'
231        curl = CurlClient(env=env)
232        r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
233            '--parallel'
234        ])
235        r.check_response(count=count, http_status=200)
236
237    @pytest.mark.parametrize("proto", ['h2', 'h3'])
238    def test_02_12_head_serial_https(self, env: Env,
239                                     httpd, nghttpx, repeat, proto):
240        if proto == 'h3' and not env.have_h3():
241            pytest.skip("h3 not supported")
242        count = 5
243        urln = f'https://{env.authority_for(env.domain1, proto)}/data-10m?[0-{count-1}]'
244        curl = CurlClient(env=env)
245        r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
246            '--head'
247        ])
248        r.check_response(count=count, http_status=200)
249
250    @pytest.mark.parametrize("proto", ['h2'])
251    def test_02_13_head_serial_h2c(self, env: Env,
252                                    httpd, nghttpx, repeat, proto):
253        if proto == 'h3' and not env.have_h3():
254            pytest.skip("h3 not supported")
255        count = 5
256        urln = f'http://{env.domain1}:{env.http_port}/data-10m?[0-{count-1}]'
257        curl = CurlClient(env=env)
258        r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
259            '--head', '--http2-prior-knowledge', '--fail-early'
260        ])
261        r.check_response(count=count, http_status=200)
262
263    @pytest.mark.parametrize("proto", ['h2', 'h3'])
264    def test_02_14_not_found(self, env: Env, httpd, nghttpx, repeat, proto):
265        if proto == 'h3' and not env.have_h3():
266            pytest.skip("h3 not supported")
267        if proto == 'h3' and env.curl_uses_lib('msh3'):
268            pytest.skip("msh3 stalls here")
269        count = 5
270        urln = f'https://{env.authority_for(env.domain1, proto)}/not-found?[0-{count-1}]'
271        curl = CurlClient(env=env)
272        r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
273            '--parallel'
274        ])
275        r.check_stats(count=count, http_status=404, exitcode=0,
276                      remote_port=env.port_for(alpn_proto=proto),
277                      remote_ip='127.0.0.1')
278
279    @pytest.mark.parametrize("proto", ['h2', 'h3'])
280    def test_02_15_fail_not_found(self, env: Env, httpd, nghttpx, repeat, proto):
281        if proto == 'h3' and not env.have_h3():
282            pytest.skip("h3 not supported")
283        if proto == 'h3' and env.curl_uses_lib('msh3'):
284            pytest.skip("msh3 stalls here")
285        count = 5
286        urln = f'https://{env.authority_for(env.domain1, proto)}/not-found?[0-{count-1}]'
287        curl = CurlClient(env=env)
288        r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
289            '--fail'
290        ])
291        r.check_stats(count=count, http_status=404, exitcode=22,
292                      remote_port=env.port_for(alpn_proto=proto),
293                      remote_ip='127.0.0.1')
294
295    @pytest.mark.skipif(condition=Env().slow_network, reason="not suitable for slow network tests")
296    @pytest.mark.skipif(condition=Env().ci_run, reason="not suitable for CI runs")
297    def test_02_20_h2_small_frames(self, env: Env, httpd, repeat):
298        # Test case to reproduce content corruption as observed in
299        # https://github.com/curl/curl/issues/10525
300        # To reliably reproduce, we need an Apache httpd that supports
301        # setting smaller frame sizes. This is not released yet, we
302        # test if it works and back out if not.
303        httpd.set_extra_config(env.domain1, lines=[
304            f'H2MaxDataFrameLen 1024',
305        ])
306        assert httpd.stop()
307        if not httpd.start():
308            # no, not supported, bail out
309            httpd.set_extra_config(env.domain1, lines=None)
310            assert httpd.start()
311            pytest.skip(f'H2MaxDataFrameLen not supported')
312        # ok, make 100 downloads with 2 parallel running and they
313        # are expected to stumble into the issue when using `lib/http2.c`
314        # from curl 7.88.0
315        count = 5
316        urln = f'https://{env.authority_for(env.domain1, "h2")}/data-1m?[0-{count-1}]'
317        curl = CurlClient(env=env)
318        r = curl.http_download(urls=[urln], alpn_proto="h2", extra_args=[
319            '--parallel', '--parallel-max', '2'
320        ])
321        r.check_response(count=count, http_status=200)
322        srcfile = os.path.join(httpd.docs_dir, 'data-1m')
323        self.check_downloads(curl, srcfile, count)
324        # restore httpd defaults
325        httpd.set_extra_config(env.domain1, lines=None)
326        assert httpd.stop()
327        assert httpd.start()
328
329    # download via lib client, 1 at a time, pause/resume at different offsets
330    @pytest.mark.parametrize("pause_offset", [0, 10*1024, 100*1023, 640000])
331    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
332    def test_02_21_lib_serial(self, env: Env, httpd, nghttpx, proto, pause_offset, repeat):
333        if proto == 'h3' and not env.have_h3():
334            pytest.skip("h3 not supported")
335        count = 2
336        docname = 'data-10m'
337        url = f'https://localhost:{env.https_port}/{docname}'
338        client = LocalClient(name='hx-download', env=env)
339        if not client.exists():
340            pytest.skip(f'example client not built: {client.name}')
341        r = client.run(args=[
342             '-n', f'{count}', '-P', f'{pause_offset}', '-V', proto, url
343        ])
344        r.check_exit_code(0)
345        srcfile = os.path.join(httpd.docs_dir, docname)
346        self.check_downloads(client, srcfile, count)
347
348    # download via lib client, several at a time, pause/resume
349    @pytest.mark.parametrize("pause_offset", [100*1023])
350    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
351    def test_02_22_lib_parallel_resume(self, env: Env, httpd, nghttpx, proto, pause_offset, repeat):
352        if proto == 'h3' and not env.have_h3():
353            pytest.skip("h3 not supported")
354        count = 2
355        max_parallel = 5
356        docname = 'data-10m'
357        url = f'https://localhost:{env.https_port}/{docname}'
358        client = LocalClient(name='hx-download', env=env)
359        if not client.exists():
360            pytest.skip(f'example client not built: {client.name}')
361        r = client.run(args=[
362            '-n', f'{count}', '-m', f'{max_parallel}',
363            '-P', f'{pause_offset}', '-V', proto, url
364        ])
365        r.check_exit_code(0)
366        srcfile = os.path.join(httpd.docs_dir, docname)
367        self.check_downloads(client, srcfile, count)
368
369    # download, several at a time, pause and abort paused
370    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
371    def test_02_23a_lib_abort_paused(self, env: Env, httpd, nghttpx, proto, repeat):
372        if proto == 'h3' and not env.have_h3():
373            pytest.skip("h3 not supported")
374        if proto == 'h3' and env.curl_uses_ossl_quic():
375            pytest.skip('OpenSSL QUIC fails here')
376        if proto == 'h3' and env.ci_run and env.curl_uses_lib('quiche'):
377            pytest.skip("fails in CI, but works locally for unknown reasons")
378        count = 10
379        max_parallel = 5
380        if proto in ['h2', 'h3']:
381            pause_offset = 64 * 1024
382        else:
383            pause_offset = 12 * 1024
384        docname = 'data-1m'
385        url = f'https://localhost:{env.https_port}/{docname}'
386        client = LocalClient(name='hx-download', env=env)
387        if not client.exists():
388            pytest.skip(f'example client not built: {client.name}')
389        r = client.run(args=[
390            '-n', f'{count}', '-m', f'{max_parallel}', '-a',
391            '-P', f'{pause_offset}', '-V', proto, url
392        ])
393        r.check_exit_code(0)
394        srcfile = os.path.join(httpd.docs_dir, docname)
395        # downloads should be there, but not necessarily complete
396        self.check_downloads(client, srcfile, count, complete=False)
397
398    # download, several at a time, abort after n bytes
399    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
400    def test_02_23b_lib_abort_offset(self, env: Env, httpd, nghttpx, proto, repeat):
401        if proto == 'h3' and not env.have_h3():
402            pytest.skip("h3 not supported")
403        if proto == 'h3' and env.curl_uses_ossl_quic():
404            pytest.skip('OpenSSL QUIC fails here')
405        if proto == 'h3' and env.ci_run and env.curl_uses_lib('quiche'):
406            pytest.skip("fails in CI, but works locally for unknown reasons")
407        count = 10
408        max_parallel = 5
409        if proto in ['h2', 'h3']:
410            abort_offset = 64 * 1024
411        else:
412            abort_offset = 12 * 1024
413        docname = 'data-1m'
414        url = f'https://localhost:{env.https_port}/{docname}'
415        client = LocalClient(name='hx-download', env=env)
416        if not client.exists():
417            pytest.skip(f'example client not built: {client.name}')
418        r = client.run(args=[
419            '-n', f'{count}', '-m', f'{max_parallel}', '-a',
420            '-A', f'{abort_offset}', '-V', proto, url
421        ])
422        r.check_exit_code(0)
423        srcfile = os.path.join(httpd.docs_dir, docname)
424        # downloads should be there, but not necessarily complete
425        self.check_downloads(client, srcfile, count, complete=False)
426
427    # download, several at a time, abort after n bytes
428    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
429    def test_02_23c_lib_fail_offset(self, env: Env, httpd, nghttpx, proto, repeat):
430        if proto == 'h3' and not env.have_h3():
431            pytest.skip("h3 not supported")
432        if proto == 'h3' and env.curl_uses_ossl_quic():
433            pytest.skip('OpenSSL QUIC fails here')
434        if proto == 'h3' and env.ci_run and env.curl_uses_lib('quiche'):
435            pytest.skip("fails in CI, but works locally for unknown reasons")
436        count = 10
437        max_parallel = 5
438        if proto in ['h2', 'h3']:
439            fail_offset = 64 * 1024
440        else:
441            fail_offset = 12 * 1024
442        docname = 'data-1m'
443        url = f'https://localhost:{env.https_port}/{docname}'
444        client = LocalClient(name='hx-download', env=env)
445        if not client.exists():
446            pytest.skip(f'example client not built: {client.name}')
447        r = client.run(args=[
448            '-n', f'{count}', '-m', f'{max_parallel}', '-a',
449            '-F', f'{fail_offset}', '-V', proto, url
450        ])
451        r.check_exit_code(0)
452        srcfile = os.path.join(httpd.docs_dir, docname)
453        # downloads should be there, but not necessarily complete
454        self.check_downloads(client, srcfile, count, complete=False)
455
456    # speed limited download
457    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
458    def test_02_24_speed_limit(self, env: Env, httpd, nghttpx, proto, repeat):
459        if proto == 'h3' and not env.have_h3():
460            pytest.skip("h3 not supported")
461        count = 1
462        url = f'https://{env.authority_for(env.domain1, proto)}/data-1m'
463        curl = CurlClient(env=env)
464        speed_limit = 384 * 1024
465        min_duration = math.floor((1024 * 1024)/speed_limit)
466        r = curl.http_download(urls=[url], alpn_proto=proto, extra_args=[
467            '--limit-rate', f'{speed_limit}'
468        ])
469        r.check_response(count=count, http_status=200)
470        assert r.duration > timedelta(seconds=min_duration), \
471            f'rate limited transfer should take more than {min_duration}s, '\
472            f'not {r.duration}'
473
474    # make extreme parallel h2 upgrades, check invalid conn reuse
475    # before protocol switch has happened
476    def test_02_25_h2_upgrade_x(self, env: Env, httpd, repeat):
477        # not locally reproducible timeouts with certain SSL libs
478        # Since this test is about connection reuse handling, we skip
479        # it on these builds. Although we would certainly like to understand
480        # why this happens.
481        if env.curl_uses_lib('bearssl'):
482            pytest.skip('CI workflows timeout on bearssl build')
483        url = f'http://localhost:{env.http_port}/data-100k'
484        client = LocalClient(name='h2-upgrade-extreme', env=env, timeout=15)
485        if not client.exists():
486            pytest.skip(f'example client not built: {client.name}')
487        r = client.run(args=[url])
488        assert r.exit_code == 0, f'{client.dump_logs()}'
489
490    # Special client that tests TLS session reuse in parallel transfers
491    # TODO: just uses a single connection for h2/h3. Not sure how to prevent that
492    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
493    def test_02_26_session_shared_reuse(self, env: Env, proto, httpd, nghttpx, repeat):
494        url = f'https://{env.authority_for(env.domain1, proto)}/data-100k'
495        client = LocalClient(name='tls-session-reuse', env=env)
496        if not client.exists():
497            pytest.skip(f'example client not built: {client.name}')
498        r = client.run(args=[proto, url])
499        r.check_exit_code(0)
500
501    # test on paused transfers, based on issue #11982
502    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
503    def test_02_27a_paused_no_cl(self, env: Env, httpd, nghttpx, proto, repeat):
504        url = f'https://{env.authority_for(env.domain1, proto)}' \
505            '/curltest/tweak/?&chunks=6&chunk_size=8000'
506        client = LocalClient(env=env, name='h2-pausing')
507        r = client.run(args=['-V', proto, url])
508        r.check_exit_code(0)
509
510    # test on paused transfers, based on issue #11982
511    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
512    def test_02_27b_paused_no_cl(self, env: Env, httpd, nghttpx, proto, repeat):
513        url = f'https://{env.authority_for(env.domain1, proto)}' \
514            '/curltest/tweak/?error=502'
515        client = LocalClient(env=env, name='h2-pausing')
516        r = client.run(args=['-V', proto, url])
517        r.check_exit_code(0)
518
519    # test on paused transfers, based on issue #11982
520    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
521    def test_02_27c_paused_no_cl(self, env: Env, httpd, nghttpx, proto, repeat):
522        url = f'https://{env.authority_for(env.domain1, proto)}' \
523            '/curltest/tweak/?status=200&chunks=1&chunk_size=100'
524        client = LocalClient(env=env, name='h2-pausing')
525        r = client.run(args=['-V', proto, url])
526        r.check_exit_code(0)
527
528    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
529    def test_02_28_get_compressed(self, env: Env, httpd, nghttpx, repeat, proto):
530        if proto == 'h3' and not env.have_h3():
531            pytest.skip("h3 not supported")
532        count = 1
533        urln = f'https://{env.authority_for(env.domain1brotli, proto)}/data-100k?[0-{count-1}]'
534        curl = CurlClient(env=env)
535        r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[
536            '--compressed'
537        ])
538        r.check_exit_code(code=0)
539        r.check_response(count=count, http_status=200)
540
541    def check_downloads(self, client, srcfile: str, count: int,
542                        complete: bool = True):
543        for i in range(count):
544            dfile = client.download_file(i)
545            assert os.path.exists(dfile)
546            if complete and not filecmp.cmp(srcfile, dfile, shallow=False):
547                diff = "".join(difflib.unified_diff(a=open(srcfile).readlines(),
548                                                    b=open(dfile).readlines(),
549                                                    fromfile=srcfile,
550                                                    tofile=dfile,
551                                                    n=1))
552                assert False, f'download {dfile} differs:\n{diff}'
553
554    # download via lib client, 1 at a time, pause/resume at different offsets
555    @pytest.mark.parametrize("pause_offset", [0, 10*1024, 100*1023, 640000])
556    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
557    def test_02_29_h2_lib_serial(self, env: Env, httpd, nghttpx, proto, pause_offset, repeat):
558        count = 2
559        docname = 'data-10m'
560        url = f'https://localhost:{env.https_port}/{docname}'
561        client = LocalClient(name='hx-download', env=env)
562        if not client.exists():
563            pytest.skip(f'example client not built: {client.name}')
564        r = client.run(args=[
565             '-n', f'{count}', '-P', f'{pause_offset}', '-V', proto, url
566        ])
567        r.check_exit_code(0)
568        srcfile = os.path.join(httpd.docs_dir, docname)
569        self.check_downloads(client, srcfile, count)
570
571    # download parallel with prior knowledge
572    def test_02_30_parallel_prior_knowledge(self, env: Env, httpd):
573        count = 3
574        curl = CurlClient(env=env)
575        urln = f'http://{env.domain1}:{env.http_port}/data.json?[0-{count-1}]'
576        r = curl.http_download(urls=[urln], extra_args=[
577            '--parallel', '--http2-prior-knowledge'
578        ])
579        r.check_response(http_status=200, count=count)
580        assert r.total_connects == 1, r.dump_logs()
581
582    # download parallel with h2 "Upgrade:"
583    def test_02_31_parallel_upgrade(self, env: Env, httpd):
584        count = 3
585        curl = CurlClient(env=env)
586        urln = f'http://{env.domain1}:{env.http_port}/data.json?[0-{count-1}]'
587        r = curl.http_download(urls=[urln], extra_args=[
588            '--parallel', '--http2'
589        ])
590        r.check_response(http_status=200, count=count)
591        # we see 3 connections, because Apache only every serves a single
592        # request via Upgrade: and then closed the connection.
593        assert r.total_connects == 3, r.dump_logs()
594