Gentoo Archives: gentoo-portage-dev

From: Zac Medico <zmedico@g.o>
To: gentoo-portage-dev@l.g.o
Cc: Zac Medico <zmedico@g.o>
Subject: [gentoo-portage-dev] [PATCH] fetch: atomic downloads (bug 175612)
Date: Sun, 28 Apr 2019 09:18:51
Message-Id: 20190428091444.1037-1-zmedico@gentoo.org
1 Direct FETCHCOMMAND/RESUMECOMMAND output to a temporary file with
2 a constant .__download__ suffix, and atomically rename the file
3 to remove the suffix only after the download has completed
4 successfully (includes digest verification when applicable).
5 Also add unit tests to cover most fetch cases.
6
7 Bug: https://bugs.gentoo.org/175612
8 Signed-off-by: Zac Medico <zmedico@g.o>
9 ---
10 lib/_emerge/BinpkgVerifier.py | 2 +-
11 lib/portage/package/ebuild/fetch.py | 97 ++++++++-----
12 lib/portage/tests/ebuild/test_fetch.py | 186 +++++++++++++++++++++++++
13 3 files changed, 251 insertions(+), 34 deletions(-)
14 create mode 100644 lib/portage/tests/ebuild/test_fetch.py
15
16 diff --git a/lib/_emerge/BinpkgVerifier.py b/lib/_emerge/BinpkgVerifier.py
17 index 7a6d15e80..bde1328ea 100644
18 --- a/lib/_emerge/BinpkgVerifier.py
19 +++ b/lib/_emerge/BinpkgVerifier.py
20 @@ -108,7 +108,7 @@ class BinpkgVerifier(CompositeTask):
21 def _digest_exception(self, name, value, expected):
22
23 head, tail = os.path.split(self._pkg_path)
24 - temp_filename = _checksum_failure_temp_file(head, tail)
25 + temp_filename = _checksum_failure_temp_file(self.pkg.root_config.settings, head, tail)
26
27 self.scheduler.output((
28 "\n!!! Digest verification failed:\n"
29 diff --git a/lib/portage/package/ebuild/fetch.py b/lib/portage/package/ebuild/fetch.py
30 index bfd97601c..cd4a5955c 100644
31 --- a/lib/portage/package/ebuild/fetch.py
32 +++ b/lib/portage/package/ebuild/fetch.py
33 @@ -30,7 +30,7 @@ portage.proxy.lazyimport.lazyimport(globals(),
34 )
35
36 from portage import os, selinux, shutil, _encodings, \
37 - _shell_quote, _unicode_encode
38 + _movefile, _shell_quote, _unicode_encode
39 from portage.checksum import (get_valid_checksum_keys, perform_md5, verify_all,
40 _filter_unaccelarated_hashes, _hash_filter, _apply_hash_filter)
41 from portage.const import BASH_BINARY, CUSTOM_MIRRORS_FILE, \
42 @@ -46,6 +46,8 @@ from portage.util import apply_recursive_permissions, \
43 varexpand, writemsg, writemsg_level, writemsg_stdout
44 from portage.process import spawn
45
46 +_download_suffix = '.__download__'
47 +
48 _userpriv_spawn_kwargs = (
49 ("uid", portage_uid),
50 ("gid", portage_gid),
51 @@ -139,7 +141,7 @@ def _userpriv_test_write_file(settings, file_path):
52 _userpriv_test_write_file_cache[file_path] = rval
53 return rval
54
55 -def _checksum_failure_temp_file(distdir, basename):
56 +def _checksum_failure_temp_file(settings, distdir, basename):
57 """
58 First try to find a duplicate temp file with the same checksum and return
59 that filename if available. Otherwise, use mkstemp to create a new unique
60 @@ -149,9 +151,13 @@ def _checksum_failure_temp_file(distdir, basename):
61 """
62
63 filename = os.path.join(distdir, basename)
64 + if basename.endswith(_download_suffix):
65 + normal_basename = basename[:-len(_download_suffix)]
66 + else:
67 + normal_basename = basename
68 size = os.stat(filename).st_size
69 checksum = None
70 - tempfile_re = re.compile(re.escape(basename) + r'\._checksum_failure_\..*')
71 + tempfile_re = re.compile(re.escape(normal_basename) + r'\._checksum_failure_\..*')
72 for temp_filename in os.listdir(distdir):
73 if not tempfile_re.match(temp_filename):
74 continue
75 @@ -173,9 +179,9 @@ def _checksum_failure_temp_file(distdir, basename):
76 return temp_filename
77
78 fd, temp_filename = \
79 - tempfile.mkstemp("", basename + "._checksum_failure_.", distdir)
80 + tempfile.mkstemp("", normal_basename + "._checksum_failure_.", distdir)
81 os.close(fd)
82 - os.rename(filename, temp_filename)
83 + _movefile(filename, temp_filename, mysettings=settings)
84 return temp_filename
85
86 def _check_digests(filename, digests, show_errors=1):
87 @@ -602,6 +608,7 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
88 pruned_digests["size"] = size
89
90 myfile_path = os.path.join(mysettings["DISTDIR"], myfile)
91 + download_path = myfile_path + _download_suffix
92 has_space = True
93 has_space_superuser = True
94 file_lock = None
95 @@ -679,12 +686,15 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
96 del e
97 continue
98
99 - if distdir_writable and mystat is None:
100 - # Remove broken symlinks if necessary.
101 + # Remove broken symlinks or symlinks to files which
102 + # _check_distfile did not match above.
103 + if distdir_writable and mystat is None or os.path.islink(myfile_path):
104 try:
105 os.unlink(myfile_path)
106 - except OSError:
107 - pass
108 + except OSError as e:
109 + if e.errno not in (errno.ENOENT, errno.ESTALE):
110 + raise
111 + mystat = None
112
113 if mystat is not None:
114 if stat.S_ISDIR(mystat.st_mode):
115 @@ -695,10 +705,28 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
116 level=logging.ERROR, noiselevel=-1)
117 return 0
118
119 + # Since _check_distfile did not match above, the file
120 + # is either corrupt or its identity has changed since
121 + # the last time it was fetched, so rename it.
122 + temp_filename = \
123 + _checksum_failure_temp_file(
124 + mysettings, mysettings["DISTDIR"], myfile)
125 + writemsg_stdout(_("Refetching... "
126 + "File renamed to '%s'\n\n") % \
127 + temp_filename, noiselevel=-1)
128 +
129 + # Stat the temporary download file for comparison with
130 + # fetch_resume_size.
131 + try:
132 + mystat = os.stat(download_path)
133 + except OSError:
134 + mystat = None
135 +
136 + if mystat is not None:
137 if mystat.st_size == 0:
138 if distdir_writable:
139 try:
140 - os.unlink(myfile_path)
141 + os.unlink(download_path)
142 except OSError:
143 pass
144 elif distdir_writable and size is not None:
145 @@ -717,14 +745,14 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
146 "ME_MIN_SIZE)\n") % mystat.st_size)
147 temp_filename = \
148 _checksum_failure_temp_file(
149 - mysettings["DISTDIR"], myfile)
150 + mysettings, mysettings["DISTDIR"], os.path.basename(download_path))
151 writemsg_stdout(_("Refetching... "
152 "File renamed to '%s'\n\n") % \
153 temp_filename, noiselevel=-1)
154 elif mystat.st_size >= size:
155 temp_filename = \
156 _checksum_failure_temp_file(
157 - mysettings["DISTDIR"], myfile)
158 + mysettings, mysettings["DISTDIR"], os.path.basename(download_path))
159 writemsg_stdout(_("Refetching... "
160 "File renamed to '%s'\n\n") % \
161 temp_filename, noiselevel=-1)
162 @@ -766,7 +794,7 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
163 for mydir in fsmirrors:
164 mirror_file = os.path.join(mydir, myfile)
165 try:
166 - shutil.copyfile(mirror_file, myfile_path)
167 + shutil.copyfile(mirror_file, download_path)
168 writemsg(_("Local mirror has file: %s\n") % myfile)
169 break
170 except (IOError, OSError) as e:
171 @@ -775,7 +803,7 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
172 del e
173
174 try:
175 - mystat = os.stat(myfile_path)
176 + mystat = os.stat(download_path)
177 except OSError as e:
178 if e.errno not in (errno.ENOENT, errno.ESTALE):
179 raise
180 @@ -784,13 +812,13 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
181 # Skip permission adjustment for symlinks, since we don't
182 # want to modify anything outside of the primary DISTDIR,
183 # and symlinks typically point to PORTAGE_RO_DISTDIRS.
184 - if not os.path.islink(myfile_path):
185 + if not os.path.islink(download_path):
186 try:
187 - apply_secpass_permissions(myfile_path,
188 + apply_secpass_permissions(download_path,
189 gid=portage_gid, mode=0o664, mask=0o2,
190 stat_cached=mystat)
191 except PortageException as e:
192 - if not os.access(myfile_path, os.R_OK):
193 + if not os.access(download_path, os.R_OK):
194 writemsg(_("!!! Failed to adjust permissions:"
195 " %s\n") % (e,), noiselevel=-1)
196
197 @@ -799,7 +827,7 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
198 if mystat.st_size == 0:
199 if distdir_writable:
200 try:
201 - os.unlink(myfile_path)
202 + os.unlink(download_path)
203 except EnvironmentError:
204 pass
205 elif myfile not in mydigests:
206 @@ -824,7 +852,7 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
207 digests = _filter_unaccelarated_hashes(mydigests[myfile])
208 if hash_filter is not None:
209 digests = _apply_hash_filter(digests, hash_filter)
210 - verified_ok, reason = verify_all(myfile_path, digests)
211 + verified_ok, reason = verify_all(download_path, digests)
212 if not verified_ok:
213 writemsg(_("!!! Previously fetched"
214 " file: '%s'\n") % myfile, noiselevel=-1)
215 @@ -838,11 +866,12 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
216 if distdir_writable:
217 temp_filename = \
218 _checksum_failure_temp_file(
219 - mysettings["DISTDIR"], myfile)
220 + mysettings, mysettings["DISTDIR"], os.path.basename(download_path))
221 writemsg_stdout(_("Refetching... "
222 "File renamed to '%s'\n\n") % \
223 temp_filename, noiselevel=-1)
224 else:
225 + _movefile(download_path, myfile_path, mysettings=mysettings)
226 eout = EOutput()
227 eout.quiet = \
228 mysettings.get("PORTAGE_QUIET", None) == "1"
229 @@ -928,7 +957,7 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
230 if not can_fetch:
231 if fetched != 2:
232 try:
233 - mysize = os.stat(myfile_path).st_size
234 + mysize = os.stat(download_path).st_size
235 except OSError as e:
236 if e.errno not in (errno.ENOENT, errno.ESTALE):
237 raise
238 @@ -952,7 +981,7 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
239 #we either need to resume or start the download
240 if fetched == 1:
241 try:
242 - mystat = os.stat(myfile_path)
243 + mystat = os.stat(download_path)
244 except OSError as e:
245 if e.errno not in (errno.ENOENT, errno.ESTALE):
246 raise
247 @@ -964,7 +993,7 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
248 "%d (smaller than " "PORTAGE_FETCH_RESU"
249 "ME_MIN_SIZE)\n") % mystat.st_size)
250 try:
251 - os.unlink(myfile_path)
252 + os.unlink(download_path)
253 except OSError as e:
254 if e.errno not in \
255 (errno.ENOENT, errno.ESTALE):
256 @@ -984,7 +1013,7 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
257 _hide_url_passwd(loc))
258 variables = {
259 "URI": loc,
260 - "FILE": myfile
261 + "FILE": os.path.basename(download_path)
262 }
263
264 for k in ("DISTDIR", "PORTAGE_SSH_OPTS"):
265 @@ -1001,12 +1030,12 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
266
267 finally:
268 try:
269 - apply_secpass_permissions(myfile_path,
270 + apply_secpass_permissions(download_path,
271 gid=portage_gid, mode=0o664, mask=0o2)
272 except FileNotFound:
273 pass
274 except PortageException as e:
275 - if not os.access(myfile_path, os.R_OK):
276 + if not os.access(download_path, os.R_OK):
277 writemsg(_("!!! Failed to adjust permissions:"
278 " %s\n") % str(e), noiselevel=-1)
279 del e
280 @@ -1015,8 +1044,8 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
281 # trust the return value from the fetcher. Remove the
282 # empty file and try to download again.
283 try:
284 - if os.stat(myfile_path).st_size == 0:
285 - os.unlink(myfile_path)
286 + if os.stat(download_path).st_size == 0:
287 + os.unlink(download_path)
288 fetched = 0
289 continue
290 except EnvironmentError:
291 @@ -1024,7 +1053,7 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
292
293 if mydigests is not None and myfile in mydigests:
294 try:
295 - mystat = os.stat(myfile_path)
296 + mystat = os.stat(download_path)
297 except OSError as e:
298 if e.errno not in (errno.ENOENT, errno.ESTALE):
299 raise
300 @@ -1065,13 +1094,13 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
301 if (mystat[stat.ST_SIZE]<100000) and (len(myfile)>4) and not ((myfile[-5:]==".html") or (myfile[-4:]==".htm")):
302 html404=re.compile("<title>.*(not found|404).*</title>",re.I|re.M)
303 with io.open(
304 - _unicode_encode(myfile_path,
305 + _unicode_encode(download_path,
306 encoding=_encodings['fs'], errors='strict'),
307 mode='r', encoding=_encodings['content'], errors='replace'
308 ) as f:
309 if html404.search(f.read()):
310 try:
311 - os.unlink(mysettings["DISTDIR"]+"/"+myfile)
312 + os.unlink(download_path)
313 writemsg(_(">>> Deleting invalid distfile. (Improper 404 redirect from server.)\n"))
314 fetched = 0
315 continue
316 @@ -1087,7 +1116,7 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
317 digests = _filter_unaccelarated_hashes(mydigests[myfile])
318 if hash_filter is not None:
319 digests = _apply_hash_filter(digests, hash_filter)
320 - verified_ok, reason = verify_all(myfile_path, digests)
321 + verified_ok, reason = verify_all(download_path, digests)
322 if not verified_ok:
323 writemsg(_("!!! Fetched file: %s VERIFY FAILED!\n") % myfile,
324 noiselevel=-1)
325 @@ -1099,7 +1128,7 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
326 return 0
327 temp_filename = \
328 _checksum_failure_temp_file(
329 - mysettings["DISTDIR"], myfile)
330 + mysettings, mysettings["DISTDIR"], os.path.basename(download_path))
331 writemsg_stdout(_("Refetching... "
332 "File renamed to '%s'\n\n") % \
333 temp_filename, noiselevel=-1)
334 @@ -1119,6 +1148,7 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
335 checksum_failure_max_tries:
336 break
337 else:
338 + _movefile(download_path, myfile_path, mysettings=mysettings)
339 eout = EOutput()
340 eout.quiet = mysettings.get("PORTAGE_QUIET", None) == "1"
341 if digests:
342 @@ -1130,6 +1160,7 @@ def fetch(myuris, mysettings, listonly=0, fetchonly=0,
343 else:
344 if not myret:
345 fetched=2
346 + _movefile(download_path, myfile_path, mysettings=mysettings)
347 break
348 elif mydigests!=None:
349 writemsg(_("No digest file available and download failed.\n\n"),
350 diff --git a/lib/portage/tests/ebuild/test_fetch.py b/lib/portage/tests/ebuild/test_fetch.py
351 new file mode 100644
352 index 000000000..d345f7703
353 --- /dev/null
354 +++ b/lib/portage/tests/ebuild/test_fetch.py
355 @@ -0,0 +1,186 @@
356 +# Copyright 2019 Gentoo Authors
357 +# Distributed under the terms of the GNU General Public License v2
358 +
359 +from __future__ import unicode_literals
360 +
361 +import tempfile
362 +
363 +import portage
364 +from portage import shutil, os
365 +from portage.tests import TestCase
366 +from portage.tests.resolver.ResolverPlayground import ResolverPlayground
367 +from portage.tests.util.test_socks5 import AsyncHTTPServer
368 +from portage.util._async.SchedulerInterface import SchedulerInterface
369 +from portage.util._eventloop.global_event_loop import global_event_loop
370 +from portage.package.ebuild.config import config
371 +from _emerge.EbuildFetcher import EbuildFetcher
372 +from _emerge.Package import Package
373 +
374 +
375 +class EbuildFetchTestCase(TestCase):
376 +
377 + def testEbuildFetch(self):
378 +
379 + distfiles = {
380 + 'bar': b'bar\n',
381 + 'foo': b'foo\n',
382 + }
383 +
384 + ebuilds = {
385 + 'dev-libs/A-1': {
386 + 'EAPI': '7',
387 + 'RESTRICT': 'primaryuri',
388 + 'SRC_URI': '''{scheme}://{host}:{port}/distfiles/bar.txt -> bar
389 + {scheme}://{host}:{port}/distfiles/foo.txt -> foo''',
390 + },
391 + }
392 +
393 + loop = SchedulerInterface(global_event_loop())
394 + scheme = 'http'
395 + host = '127.0.0.1'
396 + content = {}
397 + for k, v in distfiles.items():
398 + content['/distfiles/{}.txt'.format(k)] = v
399 +
400 + with AsyncHTTPServer(host, content, loop) as server:
401 + ebuilds_subst = {}
402 + for cpv, metadata in ebuilds.items():
403 + metadata = metadata.copy()
404 + metadata['SRC_URI'] = metadata['SRC_URI'].format(
405 + scheme=scheme, host=host, port=server.server_port)
406 + ebuilds_subst[cpv] = metadata
407 +
408 + playground = ResolverPlayground(ebuilds=ebuilds_subst, distfiles=distfiles)
409 + ro_distdir = tempfile.mkdtemp()
410 + try:
411 + fetchcommand = portage.util.shlex_split(playground.settings['FETCHCOMMAND'])
412 + fetch_bin = portage.process.find_binary(fetchcommand[0])
413 + if fetch_bin is None:
414 + self.skipTest('FETCHCOMMAND not found: {}'.format(playground.settings['FETCHCOMMAND']))
415 + root_config = playground.trees[playground.eroot]['root_config']
416 + portdb = root_config.trees["porttree"].dbapi
417 + settings = config(clone=playground.settings)
418 +
419 + # Tests only work with one ebuild at a time, so the config
420 + # pool only needs a single config instance.
421 + class config_pool:
422 + @staticmethod
423 + def allocate():
424 + return settings
425 + @staticmethod
426 + def deallocate(settings):
427 + pass
428 +
429 + def async_fetch(pkg, ebuild_path):
430 + fetcher = EbuildFetcher(config_pool=config_pool, ebuild_path=ebuild_path,
431 + fetchonly=False, fetchall=True, pkg=pkg, scheduler=loop)
432 + fetcher.start()
433 + return fetcher.async_wait()
434 +
435 + for cpv in ebuilds:
436 + metadata = dict(zip(Package.metadata_keys,
437 + portdb.aux_get(cpv, Package.metadata_keys)))
438 +
439 + pkg = Package(built=False, cpv=cpv, installed=False,
440 + metadata=metadata, root_config=root_config,
441 + type_name='ebuild')
442 +
443 + settings.setcpv(pkg)
444 + ebuild_path = portdb.findname(pkg.cpv)
445 + portage.doebuild_environment(ebuild_path, 'fetch', settings=settings, db=portdb)
446 +
447 + # Test good files in DISTDIR
448 + for k in settings['AA'].split():
449 + os.stat(os.path.join(settings['DISTDIR'], k))
450 + self.assertEqual(loop.run_until_complete(async_fetch(pkg, ebuild_path)), 0)
451 + for k in settings['AA'].split():
452 + with open(os.path.join(settings['DISTDIR'], k), 'rb') as f:
453 + self.assertEqual(f.read(), distfiles[k])
454 +
455 + # Test missing files in DISTDIR
456 + for k in settings['AA'].split():
457 + os.unlink(os.path.join(settings['DISTDIR'], k))
458 + self.assertEqual(loop.run_until_complete(async_fetch(pkg, ebuild_path)), 0)
459 + for k in settings['AA'].split():
460 + with open(os.path.join(settings['DISTDIR'], k), 'rb') as f:
461 + self.assertEqual(f.read(), distfiles[k])
462 +
463 + # Test empty files in DISTDIR
464 + for k in settings['AA'].split():
465 + file_path = os.path.join(settings['DISTDIR'], k)
466 + with open(file_path, 'wb') as f:
467 + pass
468 + self.assertEqual(os.stat(file_path).st_size, 0)
469 + self.assertEqual(loop.run_until_complete(async_fetch(pkg, ebuild_path)), 0)
470 + for k in settings['AA'].split():
471 + with open(os.path.join(settings['DISTDIR'], k), 'rb') as f:
472 + self.assertEqual(f.read(), distfiles[k])
473 +
474 + # Test non-empty files containing null bytes in DISTDIR
475 + for k in settings['AA'].split():
476 + file_path = os.path.join(settings['DISTDIR'], k)
477 + with open(file_path, 'wb') as f:
478 + for i in range(len(distfiles[k])):
479 + f.write(b'\0')
480 + self.assertEqual(os.stat(file_path).st_size, len(distfiles[k]))
481 + self.assertEqual(loop.run_until_complete(async_fetch(pkg, ebuild_path)), 0)
482 + for k in settings['AA'].split():
483 + with open(os.path.join(settings['DISTDIR'], k), 'rb') as f:
484 + self.assertEqual(f.read(), distfiles[k])
485 +
486 + # Test PORTAGE_RO_DISTDIRS
487 + settings['PORTAGE_RO_DISTDIRS'] = '"{}"'.format(ro_distdir)
488 + try:
489 + for k in settings['AA'].split():
490 + file_path = os.path.join(settings['DISTDIR'], k)
491 + os.rename(file_path, os.path.join(ro_distdir, k))
492 + self.assertEqual(loop.run_until_complete(async_fetch(pkg, ebuild_path)), 0)
493 + for k in settings['AA'].split():
494 + file_path = os.path.join(settings['DISTDIR'], k)
495 + self.assertTrue(os.path.islink(file_path))
496 + with open(file_path, 'rb') as f:
497 + self.assertEqual(f.read(), distfiles[k])
498 + os.unlink(file_path)
499 + finally:
500 + settings.pop('PORTAGE_RO_DISTDIRS')
501 +
502 + # Test local filesystem in GENTOO_MIRRORS
503 + orig_mirrors = settings['GENTOO_MIRRORS']
504 + try:
505 + settings['GENTOO_MIRRORS'] = ro_distdir
506 + self.assertEqual(loop.run_until_complete(async_fetch(pkg, ebuild_path)), 0)
507 + for k in settings['AA'].split():
508 + with open(os.path.join(settings['DISTDIR'], k), 'rb') as f:
509 + self.assertEqual(f.read(), distfiles[k])
510 + finally:
511 + settings['GENTOO_MIRRORS'] = orig_mirrors
512 +
513 + # Test readonly DISTDIR
514 + orig_distdir_mode = os.stat(settings['DISTDIR']).st_mode
515 + try:
516 + os.chmod(settings['DISTDIR'], 0o555)
517 + self.assertEqual(loop.run_until_complete(async_fetch(pkg, ebuild_path)), 0)
518 + for k in settings['AA'].split():
519 + with open(os.path.join(settings['DISTDIR'], k), 'rb') as f:
520 + self.assertEqual(f.read(), distfiles[k])
521 + finally:
522 + os.chmod(settings['DISTDIR'], orig_distdir_mode)
523 +
524 + # Test parallel-fetch mode
525 + settings['PORTAGE_PARALLEL_FETCHONLY'] = '1'
526 + try:
527 + self.assertEqual(loop.run_until_complete(async_fetch(pkg, ebuild_path)), 0)
528 + for k in settings['AA'].split():
529 + with open(os.path.join(settings['DISTDIR'], k), 'rb') as f:
530 + self.assertEqual(f.read(), distfiles[k])
531 + for k in settings['AA'].split():
532 + os.unlink(os.path.join(settings['DISTDIR'], k))
533 + self.assertEqual(loop.run_until_complete(async_fetch(pkg, ebuild_path)), 0)
534 + for k in settings['AA'].split():
535 + with open(os.path.join(settings['DISTDIR'], k), 'rb') as f:
536 + self.assertEqual(f.read(), distfiles[k])
537 + finally:
538 + settings.pop('PORTAGE_PARALLEL_FETCHONLY')
539 + finally:
540 + shutil.rmtree(ro_distdir)
541 + playground.cleanup()
542 --
543 2.21.0