Merge lp:~sergiusens/snapcraft/less-source into lp:~snappy-dev/snapcraft/core

Proposed by Sergio Schvezov on 2015-08-28
Status: Superseded
Proposed branch: lp:~sergiusens/snapcraft/less-source
Merge into: lp:~snappy-dev/snapcraft/core
Prerequisite: lp:~sergiusens/snapcraft/less-complex-collect
Diff against target: 603 lines (+310/-190)
6 files modified
snapcraft/__init__.py (+46/-147)
snapcraft/common.py (+5/-0)
snapcraft/plugins/tar_content.py (+7/-2)
snapcraft/sources.py (+186/-0)
snapcraft/tests/test_base_plugin.py (+5/-41)
snapcraft/tests/test_sources.py (+61/-0)
To merge this branch: bzr merge lp:~sergiusens/snapcraft/less-source
Reviewer Review Type Date Requested Status
Sergio Schvezov Approve on 2015-09-01
Michael Vogt Approve on 2015-09-01
Leo Arias 2015-08-28 Approve on 2015-08-30
Review via email: mp+269547@code.launchpad.net

This proposal has been superseded by a proposal from 2015-09-01.

Commit Message

Moving source management complexity out of the base plugin

Description of the Change

Mostly fixing the 22 in the mccabe output 163:1: 'BasePlugin.get_source' 22 and decoupling source management from the plugin

To post a comment you must log in.
Leo Arias (elopio) wrote :

this mccabe check is helping a lot, nice work Sergio.

In cases like this:
363 + def __init__(self, source, source_type=None, source_tag=None, source_branch=None):
364 + super().__init__(source, source_type, source_tag, source_branch)
365 + if source_tag and source_branch:
366 + raise IncompatibleOptionsError('can\'t specify both source-tag and source-branch for a mercurial source')

If you do the argument validation before the call to super.__init__, you will safe a few instructions.
Good to have, but no blocker. +1.

review: Approve
Michael Vogt (mvo) wrote :

This is really nice work, thanks for doing this! Its a great branch that reduces the complexity and makes everything more modular and easier to read. I put some comments inline but no blockers and not really important, just my 0.02ยข :)

review: Approve
Sergio Schvezov (sergiusens) wrote :

Re approving the minor review request (code comment)

review: Approve

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'snapcraft/__init__.py'
2--- snapcraft/__init__.py 2015-08-26 08:52:09 +0000
3+++ snapcraft/__init__.py 2015-09-01 10:10:15 +0000
4@@ -16,11 +16,9 @@
5
6 import logging
7 import os
8-import re
9-import tarfile
10-import urllib.parse
11
12 import snapcraft.common
13+import snapcraft.sources
14
15
16 logger = logging.getLogger(__name__)
17@@ -63,152 +61,23 @@
18 return snapcraft.common.run(cmd, cwd=cwd, **kwargs)
19
20 def isurl(self, url):
21- return urllib.parse.urlparse(url).scheme != ""
22-
23- def pull_bzr(self, source, source_tag=None):
24- tag_opts = []
25- if source_tag:
26- tag_opts = ['-r', 'tag:' + source_tag]
27- if os.path.exists(os.path.join(self.sourcedir, ".bzr")):
28- return self.run(['bzr', 'pull'] + tag_opts + [source, '-d', self.sourcedir], cwd=os.getcwd())
29- else:
30- os.rmdir(self.sourcedir)
31- return self.run(['bzr', 'branch'] + tag_opts + [source, self.sourcedir], cwd=os.getcwd())
32-
33- def pull_hg(self, source, source_tag=None, source_branch=None):
34- if source_tag and source_branch:
35- logger.error("You can't specify both source-tag and source-branch for a mercurial source (part '%s').", self.name)
36- snapcraft.common.fatal()
37-
38- if os.path.exists(os.path.join(self.sourcedir, ".hg")):
39- ref = []
40- if source_tag:
41- ref = ['-r', source_tag]
42- elif source_branch:
43- ref = ['-b', source_branch]
44- return self.run(['hg', 'pull'] + ref + [source, ], cwd=os.getcwd())
45- else:
46- ref = []
47- if source_tag or source_branch:
48- ref = ['-u', source_tag or source_branch]
49-
50- return self.run(['hg', 'clone'] + ref + [source, self.sourcedir], cwd=os.getcwd())
51-
52- def pull_git(self, source, source_tag=None, source_branch=None):
53- if source_tag and source_branch:
54- logger.error("You can't specify both source-tag and source-branch for a git source (part '%s').", self.name)
55- snapcraft.common.fatal()
56-
57- if os.path.exists(os.path.join(self.sourcedir, ".git")):
58- refspec = 'HEAD'
59- if source_branch:
60- refspec = 'refs/heads/' + source_branch
61- elif source_tag:
62- refspec = 'refs/tags/' + source_tag
63- return self.run(['git', '-C', self.sourcedir, 'pull', source, refspec], cwd=os.getcwd())
64- else:
65- branch_opts = []
66- if source_tag or source_branch:
67- branch_opts = ['--branch', source_tag or source_branch]
68- return self.run(['git', 'clone'] + branch_opts + [source, self.sourcedir], cwd=os.getcwd())
69-
70- def pull_tarball(self, source, destdir=None):
71- destdir = destdir or self.sourcedir
72- if self.isurl(source):
73- return self.run(['wget', '-c', source], cwd=destdir)
74- else:
75- return True
76-
77- def extract_tarball(self, source, srcdir=None, destdir=None):
78- srcdir = srcdir or self.sourcedir
79- destdir = destdir or self.builddir
80-
81- if self.isurl(source):
82- tarball = os.path.join(srcdir, os.path.basename(source))
83- else:
84- tarball = os.path.abspath(source)
85-
86- with tarfile.open(tarball) as tar:
87- def filter_members(tar):
88- """Filters members and member names:
89- - strips common prefix
90- - bans dangerous names"""
91- members = tar.getmembers()
92- common = os.path.commonprefix([m.name for m in members])
93-
94- # commonprefix() works a character at a time and will
95- # consider "d/ab" and "d/abc" to have common prefix "d/ab";
96- # check all members either start with common dir
97- for m in members:
98- if not (m.name.startswith(common + "/") or
99- m.isdir() and m.name == common):
100- # commonprefix() didn't return a dir name; go up one
101- # level
102- common = os.path.dirname(common)
103- break
104-
105- for m in members:
106- if m.name == common:
107- continue
108- if m.name.startswith(common + "/"):
109- m.name = m.name[len(common + "/"):]
110- # strip leading "/", "./" or "../" as many times as needed
111- m.name = re.sub(r'^(\.{0,2}/)*', r'', m.name)
112- yield m
113-
114- tar.extractall(members=filter_members(tar), path=destdir)
115-
116- return True
117+ return snapcraft.common.isurl(url)
118
119 def get_source(self, source, source_type=None, source_tag=None, source_branch=None):
120- if source_type is None:
121- if source.startswith("bzr:") or source.startswith("lp:"):
122- source_type = 'bzr'
123- elif source.startswith("git:"):
124- source_type = 'git'
125- elif self.isurl(source):
126- logger.error("Unrecognized source '%s' for part '%s'.", source, self.name)
127- snapcraft.common.fatal()
128-
129- if source_type == 'bzr':
130- if source_branch:
131- logger.error("You can't specify source-branch for a bzr source (part '%s').", self.name)
132- snapcraft.common.fatal()
133- if not self.pull_bzr(source, source_tag=source_tag):
134- return False
135- if not self.run(['cp', '-Trfa', self.sourcedir, self.builddir]):
136- return False
137- elif source_type == 'git':
138- if not self.pull_git(source, source_tag=source_tag, source_branch=source_branch):
139- return False
140- if not self.run(['cp', '-Trfa', self.sourcedir, self.builddir]):
141- return False
142- elif source_type == 'hg' or source_type == 'mercurial':
143- if not self.pull_hg(source, source_tag=source_tag, source_branch=source_branch):
144- return False
145- if not self.run(['cp', '-Trfa', self.sourcedir, self.builddir]):
146- return False
147- elif source_type == 'tar':
148- if source_branch:
149- logger.error("You can't specify source-branch for a tar source (part '%s').", self.name)
150- snapcraft.common.fatal()
151- if source_tag:
152- logger.error("You can't specify source-tag for a tar source (part '%s').", self.name)
153- snapcraft.common.fatal()
154- if not self.pull_tarball(source):
155- return False
156- if not self.extract_tarball(source):
157- return False
158- else:
159- # local source dir
160- path = os.path.abspath(source)
161- if os.path.isdir(self.builddir):
162- os.rmdir(self.builddir)
163- else:
164- os.remove(self.builddir)
165- os.symlink(path, self.builddir)
166-
167- return True
168+ try:
169+ handler_class = _get_source_handler(source_type, source)
170+ except ValueError:
171+ logger.error("Unrecognized source '%s' for part '%s'.", source, self.name)
172+ snapcraft.common.fatal()
173+
174+ try:
175+ handler = handler_class(source, self.sourcedir, source_tag, source_branch)
176+ except snapcraft.sources.IncompatibleOptionsError as e:
177+ logger.error('Issues while setting up sources for part \'%s\': %s.', self.name, e.message)
178+ snapcraft.common.fatal()
179+ if not handler.pull():
180+ return False
181+ return handler.provision(self.builddir)
182
183 def handle_source_options(self):
184 stype = getattr(self.options, 'source_type', None)
185@@ -221,3 +90,33 @@
186
187 def makedirs(self, d):
188 os.makedirs(d, exist_ok=True)
189+
190+
191+def _get_source_handler(source_type, source):
192+ if source_type is None:
193+ source_type = _get_source_type_from_uri(source)
194+
195+ if source_type == 'bzr':
196+ handler = snapcraft.sources.Bazaar
197+ elif source_type == 'git':
198+ handler = snapcraft.sources.Git
199+ elif source_type == 'mercurial' or source_type == 'hg':
200+ handler = snapcraft.sources.Mercurial
201+ elif source_type == 'tar':
202+ handler = snapcraft.sources.Tar
203+ else:
204+ handler = snapcraft.sources.Local
205+
206+ return handler
207+
208+
209+def _get_source_type_from_uri(source):
210+ source_type = ''
211+ if source.startswith("bzr:") or source.startswith("lp:"):
212+ source_type = 'bzr'
213+ elif source.startswith("git:"):
214+ source_type = 'git'
215+ elif snapcraft.common.isurl(source):
216+ raise ValueError()
217+
218+ return source_type
219
220=== modified file 'snapcraft/common.py'
221--- snapcraft/common.py 2015-08-25 20:32:40 +0000
222+++ snapcraft/common.py 2015-09-01 10:10:15 +0000
223@@ -20,6 +20,7 @@
224 import subprocess
225 import sys
226 import tempfile
227+import urllib
228
229
230 COMMAND_ORDER = ["pull", "build", "stage", "snap"]
231@@ -90,3 +91,7 @@
232
233 def get_schemadir():
234 return _schemadir
235+
236+
237+def isurl(url):
238+ return urllib.parse.urlparse(url).scheme != ""
239
240=== modified file 'snapcraft/plugins/tar_content.py'
241--- snapcraft/plugins/tar_content.py 2015-07-17 19:39:00 +0000
242+++ snapcraft/plugins/tar_content.py 2015-09-01 10:10:15 +0000
243@@ -15,12 +15,17 @@
244 # along with this program. If not, see <http://www.gnu.org/licenses/>.
245
246 import snapcraft
247+import snapcraft.sources
248
249
250 class TarContentPlugin(snapcraft.BasePlugin):
251
252+ def __init__(self, name, options):
253+ super().__init__(name, options)
254+ self.tar = snapcraft.sources.Tar(self.options.source, self.builddir)
255+
256 def pull(self):
257- return self.pull_tarball(self.options.source, destdir=self.builddir)
258+ return self.tar.pull()
259
260 def build(self):
261- return self.extract_tarball(self.options.source, srcdir=self.builddir, destdir=self.installdir)
262+ return self.tar.provision(self.installdir)
263
264=== added file 'snapcraft/sources.py'
265--- snapcraft/sources.py 1970-01-01 00:00:00 +0000
266+++ snapcraft/sources.py 2015-09-01 10:10:15 +0000
267@@ -0,0 +1,186 @@
268+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
269+#
270+# Copyright (C) 2015 Canonical Ltd
271+#
272+# This program is free software: you can redistribute it and/or modify
273+# it under the terms of the GNU General Public License version 3 as
274+# published by the Free Software Foundation.
275+#
276+# This program is distributed in the hope that it will be useful,
277+# but WITHOUT ANY WARRANTY; without even the implied warranty of
278+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
279+# GNU General Public License for more details.
280+#
281+# You should have received a copy of the GNU General Public License
282+# along with this program. If not, see <http://www.gnu.org/licenses/>.
283+
284+import logging
285+import os
286+import os.path
287+import tarfile
288+import re
289+
290+import snapcraft.common
291+
292+
293+logger = logging.getLogger(__name__)
294+
295+
296+class IncompatibleOptionsError(Exception):
297+
298+ def __init__(self, message):
299+ self.message = message
300+
301+
302+class Base:
303+
304+ def __init__(self, source, source_dir, source_tag=None, source_branch=None):
305+ self.source = source
306+ self.source_dir = source_dir
307+ self.source_tag = source_tag
308+ self.source_branch = source_branch
309+
310+ def pull(self):
311+ raise NotImplementedError('this is just a base class')
312+
313+ def provision(self, dst):
314+ return snapcraft.common.run(['cp', '-Trfa', self.source_dir, dst], cwd=os.getcwd())
315+
316+
317+class Bazaar(Base):
318+
319+ def __init__(self, source, source_dir, source_tag=None, source_branch=None):
320+ super().__init__(source, source_dir, source_tag, source_branch)
321+ if source_branch:
322+ raise IncompatibleOptionsError('can\'t specify a source-branch for a bzr source')
323+
324+ def pull(self):
325+ tag_opts = []
326+ if self.source_tag:
327+ tag_opts = ['-r', 'tag:' + self.source_tag]
328+ if os.path.exists(os.path.join(self.source_dir, ".bzr")):
329+ cmd = ['bzr', 'pull'] + tag_opts + [self.source, '-d', self.source_dir]
330+ else:
331+ os.rmdir(self.source_dir)
332+ cmd = ['bzr', 'branch'] + tag_opts + [self.source, self.source_dir]
333+
334+ return snapcraft.common.run(cmd, cwd=os.getcwd())
335+
336+
337+class Git(Base):
338+
339+ def __init__(self, source, source_dir, source_tag=None, source_branch=None):
340+ super().__init__(source, source_dir, source_tag, source_branch)
341+ if source_tag and source_branch:
342+ raise IncompatibleOptionsError('can\'t specify both source-tag and source-branch for a git source')
343+
344+ def pull(self):
345+ if os.path.exists(os.path.join(self.source_dir, ".git")):
346+ refspec = 'HEAD'
347+ if self.source_branch:
348+ refspec = 'refs/heads/' + self.source_branch
349+ elif self.source_tag:
350+ refspec = 'refs/tags/' + self.source_tag
351+ cmd = ['git', '-C', self.source_dir, 'pull', self.source, refspec]
352+ else:
353+ branch_opts = []
354+ if self.source_tag or self.source_branch:
355+ branch_opts = ['--branch', self.source_tag or self.source_branch]
356+ cmd = ['git', 'clone'] + branch_opts + [self.source, self.source_dir]
357+
358+ return snapcraft.common.run(cmd, cwd=os.getcwd())
359+
360+
361+class Mercurial(Base):
362+
363+ def __init__(self, source, source_dir, source_tag=None, source_branch=None):
364+ super().__init__(source, source_dir, source_tag, source_branch)
365+ if source_tag and source_branch:
366+ raise IncompatibleOptionsError('can\'t specify both source-tag and source-branch for a mercurial source')
367+
368+ def pull(self):
369+ if os.path.exists(os.path.join(self.source_dir, ".hg")):
370+ ref = []
371+ if self.source_tag:
372+ ref = ['-r', self.source_tag]
373+ elif self.source_branch:
374+ ref = ['-b', self.source_branch]
375+ cmd = ['hg', 'pull'] + ref + [self.source, ]
376+ else:
377+ ref = []
378+ if self.source_tag or self.source_branch:
379+ ref = ['-u', self.source_tag or self.source_branch]
380+ cmd = ['hg', 'clone'] + ref + [self.source, self.source_dir]
381+
382+ return snapcraft.common.run(cmd, cwd=os.getcwd())
383+
384+
385+class Tar(Base):
386+
387+ def __init__(self, source, source_dir, source_tag=None, source_branch=None):
388+ super().__init__(source, source_dir, source_tag, source_branch)
389+ if source_tag:
390+ raise IncompatibleOptionsError('can\'t specify a source-tag for a tar source')
391+ elif source_branch:
392+ raise IncompatibleOptionsError('can\'t specify a source-branch for a tar source')
393+
394+ def pull(self):
395+ if snapcraft.common.isurl(self.source):
396+ return snapcraft.common.run(['wget', '-q', '-c', self.source], cwd=self.source_dir)
397+ else:
398+ return True
399+
400+ def provision(self, dst):
401+ # TODO add unit tests.
402+ if snapcraft.common.isurl(self.source):
403+ tarball = os.path.join(self.source_dir, os.path.basename(self.source))
404+ else:
405+ tarball = os.path.abspath(self.source)
406+
407+ with tarfile.open(tarball) as tar:
408+ def filter_members(tar):
409+ """Filters members and member names:
410+ - strips common prefix
411+ - bans dangerous names"""
412+ members = tar.getmembers()
413+ common = os.path.commonprefix([m.name for m in members])
414+
415+ # commonprefix() works a character at a time and will
416+ # consider "d/ab" and "d/abc" to have common prefix "d/ab";
417+ # check all members either start with common dir
418+ for m in members:
419+ if not (m.name.startswith(common + "/") or
420+ m.isdir() and m.name == common):
421+ # commonprefix() didn't return a dir name; go up one
422+ # level
423+ common = os.path.dirname(common)
424+ break
425+
426+ for m in members:
427+ if m.name == common:
428+ continue
429+ if m.name.startswith(common + "/"):
430+ m.name = m.name[len(common + "/"):]
431+ # strip leading "/", "./" or "../" as many times as needed
432+ m.name = re.sub(r'^(\.{0,2}/)*', r'', m.name)
433+ yield m
434+
435+ tar.extractall(members=filter_members(tar), path=dst)
436+
437+ return True
438+
439+
440+class Local(Base):
441+
442+ def pull(self):
443+ return True
444+
445+ def provision(self, dst):
446+ path = os.path.abspath(self.source)
447+ if os.path.isdir(dst):
448+ os.rmdir(dst)
449+ else:
450+ os.remove(dst)
451+ os.symlink(path, dst)
452+
453+ return True
454
455=== modified file 'snapcraft/tests/test_base_plugin.py'
456--- snapcraft/tests/test_base_plugin.py 2015-08-05 18:13:37 +0000
457+++ snapcraft/tests/test_base_plugin.py 2015-09-01 10:10:15 +0000
458@@ -14,31 +14,14 @@
459 # You should have received a copy of the GNU General Public License
460 # along with this program. If not, see <http://www.gnu.org/licenses/>.
461
462-import http.server
463 import fixtures
464 import logging
465 import os
466-import threading
467
468 import snapcraft
469 from snapcraft import tests
470
471
472-class FakeTarballHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
473-
474- def do_GET(self):
475- data = 'Test fake tarball file'
476- self.send_response(200)
477- self.send_header('Content-Length', len(data))
478- self.send_header('Content-type', 'text/html')
479- self.end_headers()
480- self.wfile.write(data.encode())
481-
482- def log_message(self, *args):
483- # Overwritten so the test does not write to stderr.
484- pass
485-
486-
487 class TestBasePlugin(tests.TestCase):
488
489 def test_isurl(self):
490@@ -49,26 +32,6 @@
491 self.assertFalse(plugin.isurl('/foo'))
492 self.assertFalse(plugin.isurl('/fo:o'))
493
494- def test_pull_tarball_must_download_to_sourcedir(self):
495- server = http.server.HTTPServer(('', 0), FakeTarballHTTPRequestHandler)
496- server_thread = threading.Thread(target=server.serve_forever)
497- self.addCleanup(server_thread.join)
498- self.addCleanup(server.server_close)
499- self.addCleanup(server.shutdown)
500- server_thread.start()
501-
502- plugin_name = 'test_plugin'
503- dest_dir = os.path.join('parts', plugin_name, 'src')
504- os.makedirs(dest_dir)
505- tar_file_name = 'test.tar'
506- source = 'http://{}:{}/{file_name}'.format(
507- *server.server_address, file_name=tar_file_name)
508- plugin = snapcraft.BasePlugin(plugin_name, 'dummy_options')
509- plugin.pull_tarball(source)
510-
511- with open(os.path.join(dest_dir, tar_file_name), 'r') as tar_file:
512- self.assertEqual('Test fake tarball file', tar_file.read())
513-
514 def test_get_source_with_unrecognized_source_must_raise_error(self):
515 fake_logger = fixtures.FakeLogger(level=logging.ERROR)
516 self.useFixture(fake_logger)
517@@ -122,8 +85,9 @@
518
519 self.assertEqual(raised.exception.code, 1, 'Wrong exit code returned.')
520 expected = (
521- "You can't specify both source-tag and source-branch for a {} "
522- "source (part 'test_plugin').\n".format(self.source_type))
523+ 'Issues while setting up sources for part \'test_plugin\': '
524+ 'can\'t specify both source-tag and source-branch for a {} '
525+ "source.\n".format(self.source_type))
526 self.assertEqual(expected, fake_logger.output)
527
528
529@@ -159,6 +123,6 @@
530
531 self.assertEqual(raised.exception.code, 1, 'Wrong exit code returned.')
532 expected = (
533- "You can't specify {} for a {} source "
534- "(part 'test_plugin').\n".format(self.error, self.source_type))
535+ 'Issues while setting up sources for part \'test_plugin\': can\'t '
536+ 'specify a {} for a {} source.\n'.format(self.error, self.source_type))
537 self.assertEqual(expected, fake_logger.output)
538
539=== added file 'snapcraft/tests/test_sources.py'
540--- snapcraft/tests/test_sources.py 1970-01-01 00:00:00 +0000
541+++ snapcraft/tests/test_sources.py 2015-09-01 10:10:15 +0000
542@@ -0,0 +1,61 @@
543+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
544+#
545+# Copyright (C) 2015 Canonical Ltd
546+#
547+# This program is free software: you can redistribute it and/or modify
548+# it under the terms of the GNU General Public License version 3 as
549+# published by the Free Software Foundation.
550+#
551+# This program is distributed in the hope that it will be useful,
552+# but WITHOUT ANY WARRANTY; without even the implied warranty of
553+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
554+# GNU General Public License for more details.
555+#
556+# You should have received a copy of the GNU General Public License
557+# along with this program. If not, see <http://www.gnu.org/licenses/>.
558+
559+import os
560+import http.server
561+import threading
562+
563+from snapcraft import sources
564+from snapcraft import tests
565+
566+
567+class FakeTarballHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
568+
569+ def do_GET(self):
570+ data = 'Test fake tarball file'
571+ self.send_response(200)
572+ self.send_header('Content-Length', len(data))
573+ self.send_header('Content-type', 'text/html')
574+ self.end_headers()
575+ self.wfile.write(data.encode())
576+
577+ def log_message(self, *args):
578+ # Overwritten so the test does not write to stderr.
579+ pass
580+
581+
582+class TestTar(tests.TestCase):
583+
584+ def test_pull_tarball_must_download_to_sourcedir(self):
585+ server = http.server.HTTPServer(('', 0), FakeTarballHTTPRequestHandler)
586+ server_thread = threading.Thread(target=server.serve_forever)
587+ self.addCleanup(server_thread.join)
588+ self.addCleanup(server.server_close)
589+ self.addCleanup(server.shutdown)
590+ server_thread.start()
591+
592+ plugin_name = 'test_plugin'
593+ dest_dir = os.path.join('parts', plugin_name, 'src')
594+ os.makedirs(dest_dir)
595+ tar_file_name = 'test.tar'
596+ source = 'http://{}:{}/{file_name}'.format(
597+ *server.server_address, file_name=tar_file_name)
598+ tar_source = sources.Tar(source, dest_dir)
599+
600+ tar_source.pull()
601+
602+ with open(os.path.join(dest_dir, tar_file_name), 'r') as tar_file:
603+ self.assertEqual('Test fake tarball file', tar_file.read())

Subscribers

People subscribed via source and target branches