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