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