Merge lp:~jamesodhunt/ubuntu/vivid/ubuntu-core-upgrader/add-functional-tests into lp:ubuntu/vivid/ubuntu-core-upgrader
- Vivid (15.04)
- add-functional-tests
- Merge into vivid
Proposed by
James Hunt
Status: | Merged |
---|---|
Merged at revision: | 18 |
Proposed branch: | lp:~jamesodhunt/ubuntu/vivid/ubuntu-core-upgrader/add-functional-tests |
Merge into: | lp:ubuntu/vivid/ubuntu-core-upgrader |
Diff against target: |
1174 lines (+936/-170) 5 files modified
debian/changelog (+8/-0) functional/test_upgrader.py (+624/-0) ubuntucoreupgrader/tests/test_upgrader.py (+5/-169) ubuntucoreupgrader/tests/utils.py (+292/-0) ubuntucoreupgrader/upgrader.py (+7/-1) |
To merge this branch: | bzr merge lp:~jamesodhunt/ubuntu/vivid/ubuntu-core-upgrader/add-functional-tests |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Vogt | Pending | ||
Review via email: mp+251723@code.launchpad.net |
Commit message
Description of the change
* ubuntucoreupgra
- get_file_
* functional/
To post a comment you must log in.
- 18. By James Hunt
-
* Fix-up changelog.
- 19. By James Hunt
-
* Moved common test code into ubuntucoreupgra
der/tests/ utils.py.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'debian/changelog' |
2 | --- debian/changelog 2015-03-04 11:34:20 +0000 |
3 | +++ debian/changelog 2015-03-04 13:53:47 +0000 |
4 | @@ -1,3 +1,11 @@ |
5 | +ubuntu-core-upgrader (0.7.6) UNRELEASED; urgency=low |
6 | + |
7 | + * ubuntucoreupgrader/upgrader.py: |
8 | + - get_file_contents(): Fix to avoid leaking the fd. |
9 | + * functional/test_upgrader.py: Basic set of functional tests. |
10 | + |
11 | + -- James Hunt <james.hunt@ubuntu.com> Wed, 04 Mar 2015 11:52:19 +0000 |
12 | + |
13 | ubuntu-core-upgrader (0.7.5) vivid; urgency=low |
14 | |
15 | [ Michael Vogt ] |
16 | |
17 | === added directory 'functional' |
18 | === added file 'functional/__init__.py' |
19 | === added file 'functional/test_upgrader.py' |
20 | --- functional/test_upgrader.py 1970-01-01 00:00:00 +0000 |
21 | +++ functional/test_upgrader.py 2015-03-04 13:53:47 +0000 |
22 | @@ -0,0 +1,624 @@ |
23 | +#!/usr/bin/python3 |
24 | +# -*- coding: utf-8 -*- |
25 | +# -------------------------------------------------------------------- |
26 | +# Copyright © 2014-2015 Canonical Ltd. |
27 | +# |
28 | +# This program is free software: you can redistribute it and/or modify |
29 | +# it under the terms of the GNU General Public License as published by |
30 | +# the Free Software Foundation, version 3 of the License, or |
31 | +# (at your option) any later version. |
32 | +# |
33 | +# This program is distributed in the hope that it will be useful, |
34 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
35 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
36 | +# GNU General Public License for more details. |
37 | +# |
38 | +# You should have received a copy of the GNU General Public License |
39 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
40 | +# -------------------------------------------------------------------- |
41 | + |
42 | +# -------------------------------------------------------------------- |
43 | +# Functional tests for the snappy upgrader. |
44 | +# -------------------------------------------------------------------- |
45 | + |
46 | +import sys |
47 | +import os |
48 | +import logging |
49 | +import tarfile |
50 | +import unittest |
51 | +import shutil |
52 | + |
53 | +from unittest.mock import patch |
54 | + |
55 | +from ubuntucoreupgrader.upgrader import ( |
56 | + Upgrader, |
57 | + parse_args, |
58 | +) |
59 | + |
60 | +base_dir = os.path.abspath(os.path.dirname(__file__)) |
61 | +module_dir = os.path.normpath(os.path.realpath(base_dir + os.sep + '..')) |
62 | +sys.path.append(base_dir) |
63 | + |
64 | +from ubuntucoreupgrader.tests.utils import * |
65 | + |
66 | +CMD_FILE = 'ubuntu_command' |
67 | + |
68 | + |
69 | +def call_upgrader(command_file, root_dir, update): |
70 | + ''' |
71 | + Invoke the upgrader. |
72 | + |
73 | + :param command_file: commands file to drive the upgrader. |
74 | + :param root_dir: Test directory to apply the upgrade to. |
75 | + :param update: UpdateTree object. |
76 | + ''' |
77 | + |
78 | + args = [] |
79 | + args += ['--root-dir', root_dir] |
80 | + args += ['--debug', '1'] |
81 | + |
82 | + # don't delete the archive and command files. |
83 | + # The tests clean up after themselves so they will get removed then, |
84 | + # but useful to have them around to diagnose test failures. |
85 | + args.append('--leave-files') |
86 | + |
87 | + args.append(command_file) |
88 | + commands = file_to_list(command_file) |
89 | + |
90 | + cache_dir = make_tmp_dir() |
91 | + |
92 | + def mock_get_cache_dir(): |
93 | + cache_dir = update.tmp_dir |
94 | + sys_dir = os.path.join(cache_dir, 'system') |
95 | + os.makedirs(sys_dir, exist_ok=True) |
96 | + return cache_dir |
97 | + |
98 | + upgrader = Upgrader(parse_args(args), commands, []) |
99 | + upgrader.get_cache_dir = mock_get_cache_dir |
100 | + upgrader.run() |
101 | + |
102 | + shutil.rmtree(cache_dir) |
103 | + |
104 | + |
105 | +def create_device_file(path, type='c', major=-1, minor=-1): |
106 | + ''' |
107 | + Create a device file. |
108 | + |
109 | + :param path: full path to device file. |
110 | + :param type: 'c' or 'b' (character or block). |
111 | + :param major: major number. |
112 | + :param minor: minor number. |
113 | + |
114 | + XXX: This doesn't actually create a device node, |
115 | + it simply creates a regular empty file whilst ensuring the filename |
116 | + gives the impression that it is a device file. |
117 | + |
118 | + This hackery is done for the following reasons: |
119 | + |
120 | + - non-priv users cannot create device nodes (and the tests run as a |
121 | + non-priv user). |
122 | + - the removed file in the upgrade tar file does not actually specify the |
123 | + _type_ of the files to remove. Hence, we can pretend that the file |
124 | + to remove is a device file since the upgrader cannot know for sure |
125 | + (it can check the existing on-disk file, but that isn't conclusive |
126 | + since the admin may have manually modified a file, or the server |
127 | + may have generated an invalid remove file - the upgrader cannot |
128 | + know for sure. |
129 | + ''' |
130 | + assert (os.path.dirname(path).endswith('/dev')) |
131 | + |
132 | + append_file(path, 'fake-device file') |
133 | + |
134 | + |
135 | +def create_directory(path): |
136 | + ''' |
137 | + Create a directory. |
138 | + ''' |
139 | + os.makedirs(path, mode=TEST_DIR_MODE, exist_ok=False) |
140 | + |
141 | + |
142 | +def create_sym_link(source, dest): |
143 | + ''' |
144 | + Create a symbolic link. |
145 | + |
146 | + :param source: existing file to link to. |
147 | + :param dest: name for the sym link. |
148 | + ''' |
149 | + dirname = os.path.dirname(dest) |
150 | + os.makedirs(dirname, mode=TEST_DIR_MODE, exist_ok=True) |
151 | + |
152 | + os.symlink(source, dest) |
153 | + |
154 | + |
155 | +def create_hard_link(source, dest): |
156 | + ''' |
157 | + Create a hard link. |
158 | + |
159 | + :param source: existing file to link to. |
160 | + :param dest: name for the hard link. |
161 | + ''' |
162 | + os.link(source, dest) |
163 | + |
164 | + |
165 | +def is_sym_link_broken(path): |
166 | + ''' |
167 | + :param path: symbolic link to check. |
168 | + :return: True if the specified path is a broken symbolic link, |
169 | + else False. |
170 | + ''' |
171 | + try: |
172 | + os.lstat(path) |
173 | + os.stat(path) |
174 | + except: |
175 | + return True |
176 | + return False |
177 | + |
178 | + |
179 | +def make_command_file(path, update_list): |
180 | + ''' |
181 | + Create a command file that the upgrader processes. |
182 | + |
183 | + :param path: full path to file to create, |
184 | + :param update_list: list of update archives to include. |
185 | + ''' |
186 | + l = [] |
187 | + |
188 | + for file in update_list: |
189 | + l.append('update {} {}.asc'.format(file, file)) |
190 | + |
191 | + # flatten |
192 | + contents = "\n".join(l) + '\n' |
193 | + |
194 | + append_file(path, contents) |
195 | + |
196 | + |
197 | +def file_to_list(path): |
198 | + ''' |
199 | + Convert the specified file into a list and return it. |
200 | + ''' |
201 | + lines = [] |
202 | + |
203 | + with open(path, 'r') as f: |
204 | + lines = f.readlines() |
205 | + |
206 | + lines = [line.rstrip() for line in lines] |
207 | + |
208 | + return lines |
209 | + |
210 | + |
211 | +class UpgraderFileRemovalTestCase(UbuntuCoreUpgraderTestCase): |
212 | + ''' |
213 | + Test how the upgrader handles the removals file. |
214 | + ''' |
215 | + |
216 | + def test_remove_file(self): |
217 | + ''' |
218 | + Ensure the upgrader can remove a regular file. |
219 | + ''' |
220 | + |
221 | + file = 'a-regular-file' |
222 | + |
223 | + self.update.add_to_removed_file([file]) |
224 | + |
225 | + archive = self.update.create_archive(self.TARFILE) |
226 | + self.assertTrue(os.path.exists(archive)) |
227 | + |
228 | + cmd_file = os.path.join(self.update.tmp_dir, CMD_FILE) |
229 | + make_command_file(cmd_file, [self.TARFILE]) |
230 | + |
231 | + file_path = os.path.join(self.victim_dir, file) |
232 | + create_file(file_path, 'foo bar') |
233 | + |
234 | + self.assertTrue(os.path.exists(file_path)) |
235 | + self.assertTrue(os.path.isfile(file_path)) |
236 | + |
237 | + call_upgrader(cmd_file, self.victim_dir, self.update) |
238 | + |
239 | + self.assertFalse(os.path.exists(file_path)) |
240 | + |
241 | + def test_remove_directory(self): |
242 | + ''' |
243 | + Ensure the upgrader can remove a directory. |
244 | + ''' |
245 | + dir = 'a-directory' |
246 | + |
247 | + self.update.add_to_removed_file([dir]) |
248 | + |
249 | + archive = self.update.create_archive(self.TARFILE) |
250 | + self.assertTrue(os.path.exists(archive)) |
251 | + |
252 | + cmd_file = os.path.join(self.update.tmp_dir, CMD_FILE) |
253 | + make_command_file(cmd_file, [self.TARFILE]) |
254 | + |
255 | + dir_path = os.path.join(self.victim_dir, dir) |
256 | + create_directory(dir_path) |
257 | + |
258 | + self.assertTrue(os.path.exists(dir_path)) |
259 | + self.assertTrue(os.path.isdir(dir_path)) |
260 | + |
261 | + call_upgrader(cmd_file, self.victim_dir, self.update) |
262 | + |
263 | + self.assertFalse(os.path.exists(dir_path)) |
264 | + |
265 | + def test_remove_sym_link_file(self): |
266 | + ''' |
267 | + Ensure the upgrader can remove a symbolic link to a file. |
268 | + ''' |
269 | + src = 'the-source-file' |
270 | + link = 'the-symlink-file' |
271 | + |
272 | + self.update.add_to_removed_file([link]) |
273 | + |
274 | + archive = self.update.create_archive(self.TARFILE) |
275 | + self.assertTrue(os.path.exists(archive)) |
276 | + |
277 | + cmd_file = os.path.join(self.update.tmp_dir, CMD_FILE) |
278 | + make_command_file(cmd_file, [self.TARFILE]) |
279 | + |
280 | + src_file_path = os.path.join(self.victim_dir, src) |
281 | + link_file_path = os.path.join(self.victim_dir, link) |
282 | + |
283 | + create_file(src_file_path, 'foo bar') |
284 | + |
285 | + self.assertTrue(os.path.exists(src_file_path)) |
286 | + self.assertTrue(os.path.isfile(src_file_path)) |
287 | + self.assertFalse(os.path.islink(src_file_path)) |
288 | + |
289 | + create_sym_link(src_file_path, link_file_path) |
290 | + self.assertTrue(os.path.exists(link_file_path)) |
291 | + self.assertTrue(os.path.islink(link_file_path)) |
292 | + |
293 | + call_upgrader(cmd_file, self.victim_dir, self.update) |
294 | + |
295 | + # original file should still be there |
296 | + self.assertTrue(os.path.exists(src_file_path)) |
297 | + self.assertTrue(os.path.isfile(src_file_path)) |
298 | + self.assertFalse(os.path.islink(src_file_path)) |
299 | + |
300 | + # link should have gone |
301 | + self.assertFalse(os.path.exists(link_file_path)) |
302 | + |
303 | + def test_remove_sym_link_directory(self): |
304 | + ''' |
305 | + Ensure the upgrader can remove a symbolic link to a directory. |
306 | + ''' |
307 | + dir = 'the-source-directory' |
308 | + link = 'the-symlink-file' |
309 | + |
310 | + self.update.add_to_removed_file([link]) |
311 | + |
312 | + archive = self.update.create_archive(self.TARFILE) |
313 | + self.assertTrue(os.path.exists(archive)) |
314 | + |
315 | + cmd_file = os.path.join(self.update.tmp_dir, CMD_FILE) |
316 | + make_command_file(cmd_file, [self.TARFILE]) |
317 | + |
318 | + src_dir_path = os.path.join(self.victim_dir, dir) |
319 | + link_file_path = os.path.join(self.victim_dir, link) |
320 | + |
321 | + create_directory(src_dir_path) |
322 | + |
323 | + self.assertTrue(os.path.exists(src_dir_path)) |
324 | + self.assertTrue(os.path.isdir(src_dir_path)) |
325 | + self.assertFalse(os.path.islink(src_dir_path)) |
326 | + |
327 | + create_sym_link(src_dir_path, link_file_path) |
328 | + self.assertTrue(os.path.exists(link_file_path)) |
329 | + self.assertTrue(os.path.islink(link_file_path)) |
330 | + |
331 | + call_upgrader(cmd_file, self.victim_dir, self.update) |
332 | + |
333 | + # original directory should still be there |
334 | + self.assertTrue(os.path.exists(src_dir_path)) |
335 | + self.assertTrue(os.path.isdir(src_dir_path)) |
336 | + self.assertFalse(os.path.islink(src_dir_path)) |
337 | + |
338 | + # link should have gone |
339 | + self.assertFalse(os.path.exists(link_file_path)) |
340 | + |
341 | + def test_remove_hardlink(self): |
342 | + ''' |
343 | + Ensure the upgrader can remove a hard link to a file. |
344 | + ''' |
345 | + src = 'the-source-file' |
346 | + link = 'the-hardlink-file' |
347 | + |
348 | + self.update.add_to_removed_file([link]) |
349 | + |
350 | + archive = self.update.create_archive(self.TARFILE) |
351 | + self.assertTrue(os.path.exists(archive)) |
352 | + |
353 | + cmd_file = os.path.join(self.update.tmp_dir, CMD_FILE) |
354 | + make_command_file(cmd_file, [self.TARFILE]) |
355 | + |
356 | + src_file_path = os.path.join(self.victim_dir, src) |
357 | + link_file_path = os.path.join(self.victim_dir, link) |
358 | + |
359 | + create_file(src_file_path, 'foo bar') |
360 | + |
361 | + src_inode = os.stat(src_file_path).st_ino |
362 | + |
363 | + self.assertTrue(os.path.exists(src_file_path)) |
364 | + self.assertTrue(os.path.isfile(src_file_path)) |
365 | + |
366 | + create_hard_link(src_file_path, link_file_path) |
367 | + self.assertTrue(os.path.exists(link_file_path)) |
368 | + |
369 | + link_inode = os.stat(link_file_path).st_ino |
370 | + |
371 | + self.assertTrue(src_inode == link_inode) |
372 | + |
373 | + call_upgrader(cmd_file, self.victim_dir, self.update) |
374 | + |
375 | + # original file should still be there |
376 | + self.assertTrue(os.path.exists(src_file_path)) |
377 | + self.assertTrue(os.path.isfile(src_file_path)) |
378 | + |
379 | + # Inode should not have changed. |
380 | + self.assertTrue(os.stat(src_file_path).st_ino == src_inode) |
381 | + |
382 | + # link should have gone |
383 | + self.assertFalse(os.path.exists(link_file_path)) |
384 | + |
385 | + def test_remove_device_file(self): |
386 | + ''' |
387 | + Ensure the upgrader can deal with a device file. |
388 | + |
389 | + XXX: Note that The upgrader currently "deals" with them by |
390 | + XXX: ignoring them :-) |
391 | + |
392 | + ''' |
393 | + file = '/dev/a-fake-device' |
394 | + |
395 | + self.update.add_to_removed_file([file]) |
396 | + |
397 | + archive = self.update.create_archive(self.TARFILE) |
398 | + self.assertTrue(os.path.exists(archive)) |
399 | + |
400 | + cmd_file = os.path.join(self.update.tmp_dir, CMD_FILE) |
401 | + make_command_file(cmd_file, [self.TARFILE]) |
402 | + |
403 | + file_path = '{}{}'.format(self.victim_dir, file) |
404 | + |
405 | + create_device_file(file_path) |
406 | + |
407 | + self.assertTrue(os.path.exists(file_path)) |
408 | + |
409 | + # sigh - we can't assert the that filetype is a char/block |
410 | + # device because it won't be :) |
411 | + self.assertTrue(os.path.isfile(file_path)) |
412 | + |
413 | + call_upgrader(cmd_file, self.victim_dir, self.update) |
414 | + |
415 | + self.assertFalse(os.path.exists(file_path)) |
416 | + |
417 | + |
418 | +class UpgraderFileAddTestCase(UbuntuCoreUpgraderTestCase): |
419 | + ''' |
420 | + Test how the upgrader handles adding new files. |
421 | + ''' |
422 | + |
423 | + def test_create_file(self): |
424 | + ''' |
425 | + Ensure the upgrader can create a regular file. |
426 | + ''' |
427 | + file = 'created-regular-file' |
428 | + |
429 | + file_path = os.path.join(self.update.system_dir, file) |
430 | + |
431 | + create_file(file_path, 'foo bar') |
432 | + |
433 | + archive = self.update.create_archive(self.TARFILE) |
434 | + self.assertTrue(os.path.exists(archive)) |
435 | + |
436 | + cmd_file = os.path.join(self.update.tmp_dir, CMD_FILE) |
437 | + make_command_file(cmd_file, [self.TARFILE]) |
438 | + |
439 | + file_path = os.path.join(self.victim_dir, file) |
440 | + self.assertFalse(os.path.exists(file_path)) |
441 | + |
442 | + call_upgrader(cmd_file, self.victim_dir, self.update) |
443 | + |
444 | + self.assertTrue(os.path.exists(file_path)) |
445 | + self.assertTrue(os.path.isfile(file_path)) |
446 | + |
447 | + def test_create_directory(self): |
448 | + ''' |
449 | + Ensure the upgrader can create a directory. |
450 | + ''' |
451 | + dir = 'created-directory' |
452 | + |
453 | + dir_path = os.path.join(self.update.system_dir, dir) |
454 | + |
455 | + create_directory(dir_path) |
456 | + |
457 | + archive = self.update.create_archive(self.TARFILE) |
458 | + self.assertTrue(os.path.exists(archive)) |
459 | + |
460 | + cmd_file = os.path.join(self.update.tmp_dir, CMD_FILE) |
461 | + make_command_file(cmd_file, [self.TARFILE]) |
462 | + |
463 | + dir_path = os.path.join(self.victim_dir, dir) |
464 | + self.assertFalse(os.path.exists(dir_path)) |
465 | + |
466 | + call_upgrader(cmd_file, self.victim_dir, self.update) |
467 | + |
468 | + self.assertTrue(os.path.exists(dir_path)) |
469 | + self.assertTrue(os.path.isdir(dir_path)) |
470 | + |
471 | + def test_create_absolute_sym_link_to_file(self): |
472 | + ''' |
473 | + Ensure the upgrader can create a symbolic link to a file (which |
474 | + already exists and is not included in the update archive). |
475 | + ''' |
476 | + src = 'the-source-file' |
477 | + link = 'the-symlink-file' |
478 | + |
479 | + # the file the link points to should *NOT* be below the 'system/' |
480 | + # directory (since there isn't one post-unpack). |
481 | + |
482 | + # an absolute sym-link target path |
483 | + src_file_path = '/{}'.format(src) |
484 | + link_file_path = os.path.join(self.update.system_dir, link) |
485 | + |
486 | + victim_src_file_path = os.path.normpath('{}/{}' |
487 | + .format(self.victim_dir, |
488 | + src_file_path)) |
489 | + victim_link_file_path = os.path.join(self.victim_dir, link) |
490 | + |
491 | + # Create a broken sym link ('/system/<link> -> /<src>') |
492 | + create_sym_link(src_file_path, link_file_path) |
493 | + |
494 | + self.assertTrue(os.path.lexists(link_file_path)) |
495 | + self.assertTrue(os.path.islink(link_file_path)) |
496 | + self.assertTrue(is_sym_link_broken(link_file_path)) |
497 | + |
498 | + archive = self.update.create_archive(self.TARFILE) |
499 | + self.assertTrue(os.path.exists(archive)) |
500 | + |
501 | + cmd_file = os.path.join(self.update.tmp_dir, CMD_FILE) |
502 | + make_command_file(cmd_file, [self.TARFILE]) |
503 | + |
504 | + create_file(victim_src_file_path, 'foo') |
505 | + |
506 | + self.assertTrue(os.path.exists(victim_src_file_path)) |
507 | + self.assertTrue(os.path.isfile(victim_src_file_path)) |
508 | + |
509 | + self.assertFalse(os.path.exists(victim_link_file_path)) |
510 | + |
511 | + call_upgrader(cmd_file, self.victim_dir, self.update) |
512 | + |
513 | + self.assertTrue(os.path.exists(victim_src_file_path)) |
514 | + self.assertTrue(os.path.isfile(victim_src_file_path)) |
515 | + |
516 | + # upgrader should have created the link in the victim directory |
517 | + self.assertTrue(os.path.lexists(victim_link_file_path)) |
518 | + self.assertTrue(os.path.islink(victim_link_file_path)) |
519 | + self.assertFalse(is_sym_link_broken(victim_link_file_path)) |
520 | + |
521 | + def test_create_relative_sym_link_to_file(self): |
522 | + ''' |
523 | + Ensure the upgrader can create a symbolic link to a file (which |
524 | + already exists and is not included in the update archive). |
525 | + ''' |
526 | + src = 'a/b/c/the-source-file' |
527 | + link = 'a/d/e/the-symlink-file' |
528 | + |
529 | + # a relative sym-link target path |
530 | + # ##src_file_path = '../../b/c/{}'.format(src) |
531 | + src_file_path = '../../b/c/the-source-file'.format(src) |
532 | + |
533 | + # the file the link points to should *NOT* be below the 'system/' |
534 | + # directory (since there isn't one post-unpack). |
535 | + |
536 | + link_file_path = os.path.join(self.update.system_dir, link) |
537 | + |
538 | + victim_src_file_path = os.path.normpath('{}/{}' |
539 | + .format(self.victim_dir, |
540 | + src)) |
541 | + victim_link_file_path = os.path.join(self.victim_dir, link) |
542 | + |
543 | + create_sym_link(src_file_path, link_file_path) |
544 | + |
545 | + self.assertTrue(os.path.lexists(link_file_path)) |
546 | + self.assertTrue(os.path.islink(link_file_path)) |
547 | + self.assertTrue(is_sym_link_broken(link_file_path)) |
548 | + |
549 | + archive = self.update.create_archive(self.TARFILE) |
550 | + self.assertTrue(os.path.exists(archive)) |
551 | + |
552 | + cmd_file = os.path.join(self.update.tmp_dir, CMD_FILE) |
553 | + make_command_file(cmd_file, [self.TARFILE]) |
554 | + |
555 | + create_file(victim_src_file_path, 'foo') |
556 | + |
557 | + self.assertTrue(os.path.exists(victim_src_file_path)) |
558 | + self.assertTrue(os.path.isfile(victim_src_file_path)) |
559 | + |
560 | + self.assertFalse(os.path.exists(victim_link_file_path)) |
561 | + |
562 | + call_upgrader(cmd_file, self.victim_dir, self.update) |
563 | + |
564 | + self.assertTrue(os.path.exists(victim_src_file_path)) |
565 | + self.assertTrue(os.path.isfile(victim_src_file_path)) |
566 | + |
567 | + # upgrader should have created the link in the victim directory |
568 | + self.assertTrue(os.path.lexists(victim_link_file_path)) |
569 | + self.assertTrue(os.path.islink(victim_link_file_path)) |
570 | + self.assertFalse(is_sym_link_broken(victim_link_file_path)) |
571 | + |
572 | + def test_create_broken_sym_link_file(self): |
573 | + ''' |
574 | + Ensure the upgrader can create a broken symbolic link |
575 | + (one that points to a non-existent file). |
576 | + ''' |
577 | + src = 'the-source-file' |
578 | + link = 'the-symlink-file' |
579 | + |
580 | + # the file the link points to should *NOT* be below the 'system/' |
581 | + # directory (since there isn't one post-unpack). |
582 | + src_file_path = src |
583 | + |
584 | + link_file_path = os.path.join(self.update.system_dir, link) |
585 | + |
586 | + # Create a broken sym link ('/system/<link> -> /<src>') |
587 | + create_sym_link(src_file_path, link_file_path) |
588 | + |
589 | + self.assertTrue(os.path.lexists(link_file_path)) |
590 | + self.assertTrue(os.path.islink(link_file_path)) |
591 | + self.assertTrue(is_sym_link_broken(link_file_path)) |
592 | + |
593 | + archive = self.update.create_archive(self.TARFILE) |
594 | + self.assertTrue(os.path.exists(archive)) |
595 | + |
596 | + cmd_file = os.path.join(self.update.tmp_dir, CMD_FILE) |
597 | + make_command_file(cmd_file, [self.TARFILE]) |
598 | + |
599 | + victim_src_file_path = os.path.join(self.victim_dir, src) |
600 | + victim_link_file_path = os.path.join(self.victim_dir, link) |
601 | + |
602 | + self.assertFalse(os.path.exists(victim_src_file_path)) |
603 | + self.assertFalse(os.path.exists(victim_link_file_path)) |
604 | + |
605 | + call_upgrader(cmd_file, self.victim_dir, self.update) |
606 | + |
607 | + # source still shouldn't exist |
608 | + self.assertFalse(os.path.exists(victim_src_file_path)) |
609 | + |
610 | + # upgrader should have created the link in the victim directory |
611 | + self.assertTrue(os.path.lexists(victim_link_file_path)) |
612 | + self.assertTrue(os.path.islink(victim_link_file_path)) |
613 | + self.assertTrue(is_sym_link_broken(victim_link_file_path)) |
614 | + |
615 | + |
616 | +def main(): |
617 | + kwargs = {} |
618 | + format = \ |
619 | + '%(asctime)s:' \ |
620 | + '%(filename)s:' \ |
621 | + '%(name)s:' \ |
622 | + '%(funcName)s:' \ |
623 | + '%(levelname)s:' \ |
624 | + '%(message)s' |
625 | + |
626 | + kwargs['format'] = format |
627 | + |
628 | + # We want to see what's happening |
629 | + kwargs['level'] = logging.DEBUG |
630 | + |
631 | + logging.basicConfig(**kwargs) |
632 | + |
633 | + unittest.main( |
634 | + testRunner=unittest.TextTestRunner( |
635 | + stream=sys.stdout, |
636 | + verbosity=2, |
637 | + |
638 | + # don't keep running tests if one fails |
639 | + # (... who _wouldn't_ want this???) |
640 | + failfast=True |
641 | + ), |
642 | + |
643 | + ) |
644 | + |
645 | +if __name__ == '__main__': |
646 | + main() |
647 | |
648 | === modified file 'ubuntucoreupgrader/tests/test_upgrader.py' |
649 | --- ubuntucoreupgrader/tests/test_upgrader.py 2015-03-04 11:27:28 +0000 |
650 | +++ ubuntucoreupgrader/tests/test_upgrader.py 2015-03-04 13:53:47 +0000 |
651 | @@ -29,6 +29,7 @@ |
652 | import unittest |
653 | import os |
654 | import shutil |
655 | +import sys |
656 | |
657 | from unittest.mock import patch |
658 | |
659 | @@ -38,7 +39,10 @@ |
660 | parse_args, |
661 | ) |
662 | |
663 | -script_name = os.path.basename(__file__) |
664 | +base_dir = os.path.abspath(os.path.dirname(__file__)) |
665 | +sys.path.append(base_dir) |
666 | + |
667 | +from ubuntucoreupgrader.tests.utils import * |
668 | |
669 | # file mode to use for creating test directories. |
670 | TEST_DIR_MODE = 0o750 |
671 | @@ -50,19 +54,6 @@ |
672 | return parse_args([]) |
673 | |
674 | |
675 | -def make_tmp_dir(tag=None): |
676 | - ''' |
677 | - Create a temporary directory. |
678 | - ''' |
679 | - |
680 | - if tag: |
681 | - prefix = '{}-{}-'.format(script_name, tag) |
682 | - else: |
683 | - prefix = script_name |
684 | - |
685 | - return tempfile.mkdtemp(prefix=prefix) |
686 | - |
687 | - |
688 | class UpgradeTestCase(unittest.TestCase): |
689 | |
690 | def test_tar_generator_unpack_assets(self): |
691 | @@ -172,28 +163,6 @@ |
692 | shutil.rmtree(cache_dir) |
693 | |
694 | |
695 | -def append_file(path, contents): |
696 | - ''' |
697 | - Append to a regular file (create it doesn't exist). |
698 | - ''' |
699 | - |
700 | - dirname = os.path.dirname(path) |
701 | - os.makedirs(dirname, mode=TEST_DIR_MODE, exist_ok=True) |
702 | - |
703 | - with open(path, 'a') as fh: |
704 | - fh.writelines(contents) |
705 | - |
706 | - if not contents.endswith('\n'): |
707 | - fh.write('\n') |
708 | - |
709 | - |
710 | -def create_file(path, contents): |
711 | - ''' |
712 | - Create a regular file. |
713 | - ''' |
714 | - append_file(path, contents) |
715 | - |
716 | - |
717 | def touch_file(path): |
718 | ''' |
719 | Create an empty file (creating any necessary intermediate |
720 | @@ -223,139 +192,6 @@ |
721 | return l |
722 | |
723 | |
724 | -class UpdateTree(): |
725 | - ''' |
726 | - Representation of a directory tree that will be converted into an |
727 | - update archive. |
728 | - ''' |
729 | - TEST_REMOVED_FILE = 'removed' |
730 | - TEST_SYSTEM_DIR = 'system/' |
731 | - |
732 | - def __init__(self): |
733 | - |
734 | - # Directory tree used to construct the tar file from. |
735 | - # Also used to hold the TEST_REMOVED_FILE file. |
736 | - self.dir = make_tmp_dir(tag='UpdateTree-tar-source') |
737 | - |
738 | - self.removed_file = os.path.join(self.dir, self.TEST_REMOVED_FILE) |
739 | - |
740 | - # Directory to place create/modify files into. |
741 | - self.system_dir = os.path.join(self.dir, self.TEST_SYSTEM_DIR) |
742 | - |
743 | - # Directory used to write the generated tarfile to. |
744 | - # This directory should also be used to write the command file |
745 | - # to. |
746 | - self.tmp_dir = make_tmp_dir(tag='UpdateTree-cache') |
747 | - |
748 | - def destroy(self): |
749 | - if os.path.exists(self.dir): |
750 | - shutil.rmtree(self.dir) |
751 | - |
752 | - if os.path.exists(self.tmp_dir): |
753 | - shutil.rmtree(self.tmp_dir) |
754 | - |
755 | - def tar_filter(self, member): |
756 | - ''' |
757 | - Function to filter the tarinfo members before creating the |
758 | - archive. |
759 | - ''' |
760 | - # members are created with relative paths (no leading slash) |
761 | - path = os.sep + member.name |
762 | - |
763 | - if member.name == '/.': |
764 | - return None |
765 | - |
766 | - i = path.find(self.dir) |
767 | - assert(i == 0) |
768 | - |
769 | - # remove the temporary directory elements |
770 | - # (+1 for the os.sep we added above) |
771 | - member.name = path[len(self.dir)+1:] |
772 | - |
773 | - return member |
774 | - |
775 | - def create_archive(self, name): |
776 | - ''' |
777 | - Create an archive with the specified name from the UpdateTree |
778 | - object. Also creates a fake signature file alongside the archive |
779 | - file since this is currently required by the upgrader (although |
780 | - it is not validated). |
781 | - |
782 | - :param name: name of tarfile. |
783 | - :param name: full path to xz archive to create. |
784 | - :return full path to tar file with name @name. |
785 | - ''' |
786 | - |
787 | - tar_path = os.path.join(self.tmp_dir, name) |
788 | - tar = tarfile.open(tar_path, 'w:xz') |
789 | - |
790 | - # We can't just add recursively since that would attempt to add |
791 | - # the parent directory. However, the real update tars don't |
792 | - # include that, and attempting to ignore the parent directory |
793 | - # results in an empty archive. So, walk the tree and add |
794 | - # file-by-file. |
795 | - for path, names, files in os.walk(self.dir): |
796 | - for file in files: |
797 | - full = os.path.join(path, file) |
798 | - tar.add(full, recursive=False, filter=self.tar_filter) |
799 | - if not files and not names: |
800 | - # add (empty) directories |
801 | - tar.add(path, recursive=False, filter=self.tar_filter) |
802 | - |
803 | - tar.close() |
804 | - |
805 | - signature = '{}.asc'.format(tar_path) |
806 | - |
807 | - with open(signature, 'w') as fh: |
808 | - fh.write('fake signature file') |
809 | - |
810 | - return tar_path |
811 | - |
812 | - |
813 | -class UbuntuCoreUpgraderTestCase(unittest.TestCase): |
814 | - ''' |
815 | - Base class for Upgrader tests. |
816 | - ''' |
817 | - |
818 | - TARFILE = 'update.tar.xz' |
819 | - |
820 | - # result of last test run. Hack to deal with fact that even if a |
821 | - # test fails, unittest still calls .tearDown() (whomever thought |
822 | - # that was a good idea...?) |
823 | - currentResult = None |
824 | - |
825 | - def setUp(self): |
826 | - ''' |
827 | - Test setup. |
828 | - ''' |
829 | - # Create an object to hold the tree that will be converted into |
830 | - # an upgrade archive. |
831 | - self.update = UpdateTree() |
832 | - |
833 | - # The directory which will have the update archive applied to |
834 | - # it. |
835 | - self.victim_dir = make_tmp_dir(tag='victim') |
836 | - |
837 | - def tearDown(self): |
838 | - ''' |
839 | - Test cleanup. |
840 | - ''' |
841 | - |
842 | - if not self.currentResult.wasSuccessful(): |
843 | - # Do not clean up - the only sane option if a test fails. |
844 | - return |
845 | - |
846 | - self.update.destroy() |
847 | - self.update = None |
848 | - |
849 | - shutil.rmtree(self.victim_dir) |
850 | - self.victim_dir = None |
851 | - |
852 | - def run(self, result=None): |
853 | - self.currentResult = result |
854 | - unittest.TestCase.run(self, result) |
855 | - |
856 | - |
857 | def mock_get_root_partitions_by_label(): |
858 | matches = [] |
859 | |
860 | |
861 | === added file 'ubuntucoreupgrader/tests/utils.py' |
862 | --- ubuntucoreupgrader/tests/utils.py 1970-01-01 00:00:00 +0000 |
863 | +++ ubuntucoreupgrader/tests/utils.py 2015-03-04 13:53:47 +0000 |
864 | @@ -0,0 +1,292 @@ |
865 | +# -*- coding: utf-8 -*- |
866 | +# -------------------------------------------------------------------- |
867 | +# Copyright © 2014-2015 Canonical Ltd. |
868 | +# |
869 | +# This program is free software: you can redistribute it and/or modify |
870 | +# it under the terms of the GNU General Public License as published by |
871 | +# the Free Software Foundation, version 3 of the License, or |
872 | +# (at your option) any later version. |
873 | +# |
874 | +# This program is distributed in the hope that it will be useful, |
875 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
876 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
877 | +# GNU General Public License for more details. |
878 | +# |
879 | +# You should have received a copy of the GNU General Public License |
880 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
881 | +# -------------------------------------------------------------------- |
882 | + |
883 | +import os |
884 | +import tempfile |
885 | +import tarfile |
886 | +import shutil |
887 | +import unittest |
888 | + |
889 | +from unittest.mock import patch |
890 | + |
891 | +# file mode to use for creating test directories. |
892 | +TEST_DIR_MODE = 0o750 |
893 | + |
894 | +script_name = os.path.basename(__file__) |
895 | + |
896 | + |
897 | +def make_tmp_dir(tag=None): |
898 | + ''' |
899 | + Create a temporary directory. |
900 | + ''' |
901 | + |
902 | + if tag: |
903 | + prefix = '{}-{}-'.format(script_name, tag) |
904 | + else: |
905 | + prefix = script_name |
906 | + |
907 | + return tempfile.mkdtemp(prefix=prefix) |
908 | + |
909 | + |
910 | +def append_file(path, contents): |
911 | + ''' |
912 | + Append to a regular file (create it doesn't exist). |
913 | + ''' |
914 | + |
915 | + dirname = os.path.dirname(path) |
916 | + os.makedirs(dirname, mode=TEST_DIR_MODE, exist_ok=True) |
917 | + |
918 | + with open(path, 'a') as fh: |
919 | + fh.writelines(contents) |
920 | + |
921 | + if not contents.endswith('\n'): |
922 | + fh.write('\n') |
923 | + |
924 | + |
925 | +def create_file(path, contents): |
926 | + ''' |
927 | + Create a regular file. |
928 | + ''' |
929 | + append_file(path, contents) |
930 | + |
931 | + |
932 | +def mock_get_root_partitions_by_label(): |
933 | + ''' |
934 | + Fake disk partition details for testing. |
935 | + ''' |
936 | + matches = [] |
937 | + |
938 | + matches.append(('system-a', '/dev/sda3', '/')) |
939 | + matches.append(('system-b', '/dev/sda4', '/writable/cache/system')) |
940 | + |
941 | + return matches |
942 | + |
943 | + |
944 | +def mock_make_mount_private(target): |
945 | + ''' |
946 | + NOP implementation. |
947 | + ''' |
948 | + pass |
949 | + |
950 | + |
951 | +class UpdateTree(): |
952 | + ''' |
953 | + Representation of a directory tree that will be converted into an |
954 | + update archive. |
955 | + ''' |
956 | + TEST_REMOVED_FILE = 'removed' |
957 | + TEST_SYSTEM_DIR = 'system/' |
958 | + |
959 | + def __init__(self): |
960 | + |
961 | + # Directory tree used to construct the tar file from. |
962 | + # Also used to hold the TEST_REMOVED_FILE file. |
963 | + self.dir = make_tmp_dir(tag='UpdateTree-tar-source') |
964 | + |
965 | + self.removed_file = os.path.join(self.dir, self.TEST_REMOVED_FILE) |
966 | + |
967 | + # Directory to place create/modify files into. |
968 | + self.system_dir = os.path.join(self.dir, self.TEST_SYSTEM_DIR) |
969 | + |
970 | + # Directory used to write the generated tarfile to. |
971 | + # This directory should also be used to write the command file |
972 | + # to. |
973 | + self.tmp_dir = make_tmp_dir(tag='UpdateTree-cache') |
974 | + |
975 | + def destroy(self): |
976 | + if os.path.exists(self.dir): |
977 | + shutil.rmtree(self.dir) |
978 | + |
979 | + if os.path.exists(self.tmp_dir): |
980 | + shutil.rmtree(self.tmp_dir) |
981 | + |
982 | + def add_to_removed_file(self, removed_files): |
983 | + ''' |
984 | + Add the specified list of files to the removed file. |
985 | + |
986 | + The 'removed' file is simply a file with a well-known name that |
987 | + contains a list of files (one per line) to be removed from a |
988 | + system before the rest of the update archive is unpacked. |
989 | + |
990 | + :param removed_files: list of file names to add to the removed file. |
991 | + |
992 | + ''' |
993 | + # all files listed in the removed list must be system files |
994 | + final = list(map(lambda a: |
995 | + '{}{}'.format(self.TEST_SYSTEM_DIR, a), removed_files)) |
996 | + |
997 | + contents = "".join(final) |
998 | + append_file(self.removed_file, contents) |
999 | + |
1000 | + def tar_filter(self, member): |
1001 | + ''' |
1002 | + Function to filter the tarinfo members before creating the |
1003 | + archive. |
1004 | + ''' |
1005 | + # members are created with relative paths (no leading slash) |
1006 | + path = os.sep + member.name |
1007 | + |
1008 | + if member.name == '/.': |
1009 | + return None |
1010 | + |
1011 | + i = path.find(self.dir) |
1012 | + assert(i == 0) |
1013 | + |
1014 | + # remove the temporary directory elements |
1015 | + # (+1 for the os.sep we added above) |
1016 | + member.name = path[len(self.dir)+1:] |
1017 | + |
1018 | + return member |
1019 | + |
1020 | + def create_archive(self, name): |
1021 | + ''' |
1022 | + Create an archive with the specified name from the UpdateTree |
1023 | + object. Also creates a fake signature file alongside the archive |
1024 | + file since this is currently required by the upgrader (although |
1025 | + it is not validated). |
1026 | + |
1027 | + :param name: name of tarfile. |
1028 | + :param name: full path to xz archive to create. |
1029 | + :return full path to tar file with name @name. |
1030 | + ''' |
1031 | + |
1032 | + self.tar_path = os.path.join(self.tmp_dir, name) |
1033 | + tar = tarfile.open(self.tar_path, 'w:xz') |
1034 | + |
1035 | + # We can't just add recursively since that would attempt to add |
1036 | + # the parent directory. However, the real update tars don't |
1037 | + # include that, and attempting to ignore the parent directory |
1038 | + # results in an empty archive. So, walk the tree and add |
1039 | + # file-by-file. |
1040 | + for path, names, files in os.walk(self.dir): |
1041 | + for file in files: |
1042 | + full = os.path.join(path, file) |
1043 | + tar.add(full, recursive=False, filter=self.tar_filter) |
1044 | + if not files and not names: |
1045 | + # add (empty) directories |
1046 | + tar.add(path, recursive=False, filter=self.tar_filter) |
1047 | + |
1048 | + tar.close() |
1049 | + |
1050 | + signature = '{}.asc'.format(self.tar_path) |
1051 | + |
1052 | + with open(signature, 'w') as fh: |
1053 | + fh.write('fake signature file') |
1054 | + |
1055 | + return self.tar_path |
1056 | + |
1057 | + |
1058 | +class UbuntuCoreUpgraderTestCase(unittest.TestCase): |
1059 | + ''' |
1060 | + Base class for Upgrader tests. |
1061 | + |
1062 | + Most of the tests follow a standard pattern: |
1063 | + |
1064 | + 1) Create an UpdateTree object: |
1065 | + |
1066 | + update = UpdateTree() |
1067 | + |
1068 | + This creates 2 temporary directories: |
1069 | + |
1070 | + - self.system dir: Used as to generate an update archive from. |
1071 | + |
1072 | + - self.tmp_dir: Used to write the generated archive file to. The |
1073 | + intention is that this directory should also be used to hold |
1074 | + the command file. |
1075 | + |
1076 | + 2) Removal tests call update.add_to_removed_file(file) to add a |
1077 | + particular file to the removals file in the update archive. |
1078 | + |
1079 | + 3) Create/Modify tests create files below update.system_dir. |
1080 | + |
1081 | + 4) Create an update archive (which includes the removals file |
1082 | + and all files below update.system_dir): |
1083 | + |
1084 | + archive = update.create_archive(self.TARFILE) |
1085 | + |
1086 | + 5) Create a command file (which tells the upgrader what to do |
1087 | + and which archive files to apply): |
1088 | + |
1089 | + make_command_file(...) |
1090 | + |
1091 | + 6) Create a victim directory. This is a temporary directory where |
1092 | + the upgrade will happen. |
1093 | + |
1094 | + 7) Start the upgrade: |
1095 | + |
1096 | + call_upgrader(...) |
1097 | + |
1098 | + 8) Perform checks on the victim directory to ensure that upgrade |
1099 | + did what was expected. |
1100 | + |
1101 | + ''' |
1102 | + |
1103 | + TARFILE = 'update.tar.xz' |
1104 | + |
1105 | + # result of last test run. Hack to deal with fact that even if a |
1106 | + # test fails, unittest still calls .tearDown() (whomever thought |
1107 | + # that was a good idea...?) |
1108 | + currentResult = None |
1109 | + |
1110 | + def setUp(self): |
1111 | + ''' |
1112 | + Test setup. |
1113 | + ''' |
1114 | + # Create an object to hold the tree that will be converted into |
1115 | + # an upgrade archive. |
1116 | + self.update = UpdateTree() |
1117 | + |
1118 | + # The directory which will have the update archive applied to |
1119 | + # it. |
1120 | + self.victim_dir = make_tmp_dir(tag='victim') |
1121 | + |
1122 | + self.patch_get_root_partitions_by_label = \ |
1123 | + patch('ubuntucoreupgrader.upgrader.get_root_partitions_by_label', |
1124 | + mock_get_root_partitions_by_label) |
1125 | + |
1126 | + self.patch_make_mount_private = \ |
1127 | + patch('ubuntucoreupgrader.upgrader.make_mount_private', |
1128 | + mock_make_mount_private) |
1129 | + |
1130 | + self.mock_get_root_partitions_by_label = \ |
1131 | + self.patch_get_root_partitions_by_label.start() |
1132 | + |
1133 | + self.mock_make_mount_private = \ |
1134 | + self.patch_make_mount_private.start() |
1135 | + |
1136 | + def tearDown(self): |
1137 | + ''' |
1138 | + Test cleanup. |
1139 | + ''' |
1140 | + |
1141 | + if not self.currentResult.wasSuccessful(): |
1142 | + # Do not clean up - the only sane option if a test fails. |
1143 | + return |
1144 | + |
1145 | + self.update.destroy() |
1146 | + self.update = None |
1147 | + |
1148 | + shutil.rmtree(self.victim_dir) |
1149 | + self.victim_dir = None |
1150 | + |
1151 | + self.patch_get_root_partitions_by_label.stop() |
1152 | + self.patch_make_mount_private.stop() |
1153 | + |
1154 | + def run(self, result=None): |
1155 | + self.currentResult = result |
1156 | + unittest.TestCase.run(self, result) |
1157 | |
1158 | === modified file 'ubuntucoreupgrader/upgrader.py' |
1159 | --- ubuntucoreupgrader/upgrader.py 2015-03-04 11:27:28 +0000 |
1160 | +++ ubuntucoreupgrader/upgrader.py 2015-03-04 13:53:47 +0000 |
1161 | @@ -1455,7 +1455,13 @@ |
1162 | tar.extract(path=tmpdir, member=tar.getmember(file)) |
1163 | |
1164 | path = os.path.join(tmpdir, file) |
1165 | - lines = [line.rstrip() for line in open(path, 'r')] |
1166 | + |
1167 | + lines = [] |
1168 | + |
1169 | + with open(path, 'r') as f: |
1170 | + lines = f.readlines() |
1171 | + |
1172 | + lines = [line.rstrip() for line in lines] |
1173 | |
1174 | shutil.rmtree(tmpdir) |
1175 |