Merge lp:~mvo/ubuntu/vivid/ubuntu-core-upgrader/add-test-tar-generator into lp:ubuntu/vivid/ubuntu-core-upgrader

Proposed by Michael Vogt
Status: Merged
Merged at revision: 12
Proposed branch: lp:~mvo/ubuntu/vivid/ubuntu-core-upgrader/add-test-tar-generator
Merge into: lp:ubuntu/vivid/ubuntu-core-upgrader
Diff against target: 226 lines (+112/-85)
2 files modified
ubuntucoreupgrader/tests/test_upgrader.py (+26/-0)
ubuntucoreupgrader/upgrader.py (+86/-85)
To merge this branch: bzr merge lp:~mvo/ubuntu/vivid/ubuntu-core-upgrader/add-test-tar-generator
Reviewer Review Type Date Requested Status
James Hunt (community) Approve
Ubuntu branches Pending
Review via email: mp+251230@code.launchpad.net

Description of the change

This branch adds a minimal test for the tar_generator() method. With that
in place the other parts of the function (like removed_files handling etc)
can be tested.

I moved the function it out of "Upgrader" because upgrader takes the "options" argument and that caused me some headache to mock. But it does not have to be this way, if we create a MockOptions. But the advantage of having it as a independent function is that all state is explicitly passed in and we don't have to worry about internal object state.

To post a comment you must log in.
Revision history for this message
James Hunt (jamesodhunt) wrote :

Looks good as a start.

As mentioned, unfortunate that we were both working on the same code at the same time, but I can rework my code again to fit in with what we have here. Note that I had solved the options issue by passing a dict of options to the Upgrader() rather than an argparse Namespace() object.

review: Approve
Revision history for this message
Michael Vogt (mvo) wrote :

Yeah, passing a dict or a unittest.mock.Mock() will work just fine to mock the options I just didn't do it for this test but for others I'm sure it will be needed :)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'ubuntucoreupgrader/tests/test_upgrader.py'
2--- ubuntucoreupgrader/tests/test_upgrader.py 1970-01-01 00:00:00 +0000
3+++ ubuntucoreupgrader/tests/test_upgrader.py 2015-02-27 09:50:52 +0000
4@@ -0,0 +1,26 @@
5+#!/usr/bin/python
6+
7+import tarfile
8+import tempfile
9+import unittest
10+
11+from ubuntucoreupgrader.upgrader import (
12+ tar_generator,
13+)
14+
15+
16+class UpgradeTestCase(unittest.TestCase):
17+
18+ def test_tar_generator_regression_unpack_assets(self):
19+ tempf = tempfile.TemporaryFile()
20+ with tarfile.TarFile(fileobj=tempf, mode="w") as tar:
21+ tar.add(__file__, "system/bin/true")
22+ tar.add(__file__, "assets/kernel")
23+ tar.add(__file__, "unreleated/something")
24+
25+ result = [m.name for m in tar_generator(tar, "cache_dir", [], "/")]
26+ self.assertEqual(result, ["system/bin/true", "assets/kernel"])
27+
28+
29+if __name__ == "__main__":
30+ unittest.main()
31
32=== modified file 'ubuntucoreupgrader/upgrader.py'
33--- ubuntucoreupgrader/upgrader.py 2015-02-20 17:00:34 +0000
34+++ ubuntucoreupgrader/upgrader.py 2015-02-27 09:50:52 +0000
35@@ -727,6 +727,89 @@
36 return True
37
38
39+def tar_generator(tar, cache_dir, removed_files, root_dir):
40+ '''
41+ Generator function to handle extracting members from the system
42+ image tar file.
43+ '''
44+ for member in tar:
45+
46+ # Restrictions:
47+ #
48+ # - Don't unpack the removed file.
49+ # - Don't unpack device files *iff* they already exist.
50+ # - Don't unpack files that are not below
51+ # TAR_FILE_SYSTEM_PREFIX or TAR_FILE_ASSETS_PREFIX.
52+ #
53+ # - XXX: This is a temporary function to work-around for
54+ # LP: #1381121: we shouldn't need to filter which files are
55+ # extracted!
56+ device_prefix = '{}dev/'.format(TAR_FILE_SYSTEM_PREFIX)
57+
58+ mount_path = '{}/{}'.format(cache_dir, member.name)
59+
60+ unpack = True
61+
62+ if member.name == removed_files:
63+ # already handled
64+ unpack = False
65+
66+ if member.name.startswith(device_prefix) and \
67+ os.path.exists(mount_path):
68+ # device already exists
69+ unpack = False
70+
71+ if not (member.name.startswith(TAR_FILE_SYSTEM_PREFIX) or
72+ member.name.startswith(TAR_FILE_ASSETS_PREFIX)):
73+ # unrecognised prefix directory
74+ unpack = False
75+
76+ if not unpack:
77+ log.debug('not unpacking file {}'.format(member.name))
78+ else:
79+ # A modified root directory requires convering
80+ # absolute paths to be located below the modified root
81+ # directory.
82+ if root_dir != '/':
83+ base = remove_prefix(member.name)
84+ member.name = '{}{}'.format(root_dir, base)
85+
86+ if member.type in (tarfile.LNKTYPE, tarfile.SYMTYPE) \
87+ and member.linkname.startswith('/'):
88+ # Hard and symbolic links also need their
89+ # 'source' updated to take account of the root
90+ # directory.
91+ #
92+ # But rather than remove the prefix, we add the
93+ # root directory as a prefix to contain the link
94+ # within that root.
95+ base = os.path.join(root_dir, member.linkname)
96+
97+ member.linkname = '{}{}'.format(root_dir, base)
98+ else:
99+ path = mount_path
100+
101+ log.debug('unpacking file {}'.format(member.name))
102+
103+ # If the file is a binary and that binary is currently
104+ # being executed by a process, attempting to unpack it
105+ # will result in ETXTBSY (OSError: [Errno 26] Text file
106+ # busy). The simplest way around this issue is to unlink
107+ # the file just before unpacking it (ideally, we'd catch
108+ # the exception and handle it separately). This allows
109+ # the unpack to continue, and the running process to
110+ # continue to use it's (old) version of the binary until
111+ # it's corresponding service is restarted.
112+ #
113+ # Note that at this point, we already have another copy
114+ # of the inode below /lost+found/.
115+ if not member.isdir() and os.path.lexists(path):
116+ log.debug('removing file {}'.format(path))
117+ os.unlink(path)
118+
119+ yield member
120+
121+
122 class Upgrader():
123
124 # FIXME: Should query system-image-cli (see bug LP:#1380574).
125@@ -1436,90 +1519,6 @@
126 if e.errno != errno.ENOTEMPTY:
127 raise e
128
129- def tar_generator(self, members):
130- '''
131- Generator function to handle extracting members from the system
132- image tar file.
133- '''
134- for member in members:
135-
136- # Restrictions:
137- #
138- # - Don't unpack the removed file.
139- # - Don't unpack device files *iff* they already exist.
140- # - Don't unpack files that are not below
141- # TAR_FILE_SYSTEM_PREFIX or TAR_FILE_ASSETS_PREFIX.
142- #
143- # - XXX: This is a temporary function to work-around for
144- # LP: #1381121: we shouldn't need to filter which files are
145- # extracted!
146- device_prefix = '{}dev/'.format(TAR_FILE_SYSTEM_PREFIX)
147-
148- mount_path = '{}/{}'.format(self.get_cache_dir(), member.name)
149-
150- unpack = True
151-
152- if member.name == self.removed_file:
153- # already handled
154- unpack = False
155-
156- if member.name.startswith(device_prefix) and \
157- os.path.exists(mount_path):
158- # device already exists
159- unpack = False
160-
161- if not (member.name.startswith(TAR_FILE_SYSTEM_PREFIX) or
162- member.name.startswith(TAR_FILE_ASSETS_PREFIX)):
163- # unrecognised prefix directory
164- unpack = False
165-
166- if not unpack:
167- log.debug('not unpacking file {}'.format(member.name))
168- else:
169- # A modified root directory requires convering
170- # absolute paths to be located below the modified root
171- # directory.
172- if self.options.root_dir != '/':
173- base = remove_prefix(member.name)
174- member.name = '{}{}'.format(self.options.root_dir, base)
175-
176- if member.type in (tarfile.LNKTYPE, tarfile.SYMTYPE) \
177- and member.linkname.startswith('/'):
178- # Hard and symbolic links also need their
179- # 'source' updated to take account of the root
180- # directory.
181- #
182- # But rather than remove the prefix, we add the
183- # root directory as a prefix to contain the link
184- # within that root.
185- base = os.path.join(self.options.root_dir,
186- member.linkname)
187-
188- member.linkname = '{}{}'.format(self.options.root_dir,
189- base)
190- else:
191- path = mount_path
192-
193- log.debug('unpacking file {}'.format(member.name))
194-
195- # If the file is a binary and that binary is currently
196- # being executed by a process, attempting to unpack it
197- # will result in ETXTBSY (OSError: [Errno 26] Text file
198- # busy). The simplest way around this issue is to unlink
199- # the file just before unpacking it (ideally, we'd catch
200- # the exception and handle it separately). This allows
201- # the unpack to continue, and the running process to
202- # continue to use it's (old) version of the binary until
203- # it's corresponding service is restarted.
204- #
205- # Note that at this point, we already have another copy
206- # of the inode below /lost+found/.
207- if not member.isdir() and os.path.lexists(path):
208- log.debug('removing file {}'.format(path))
209- os.unlink(path)
210-
211- yield member
212-
213 def _cmd_update(self, args):
214 '''
215 Unpack a new system image.
216@@ -1667,7 +1666,9 @@
217 with patch("grp.getgrnam") as m:
218 m.side_effect = KeyError()
219 tar.extractall(path=self.get_cache_dir(),
220- members=self.tar_generator(tar))
221+ members=tar_generator(
222+ tar, self.get_cache_dir(),
223+ self.removed_file, self.options.root_dir))
224 tar.close()
225
226 if self.upgrade_type == self.upgrade_types.UPGRADE_AB_PARTITIONS:

Subscribers

People subscribed via source and target branches

to all changes: