Merge lp:~jamesodhunt/ubuntu/vivid/ubuntu-core-upgrader/add-functional-tests into lp:ubuntu/vivid/ubuntu-core-upgrader

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
Reviewer Review Type Date Requested Status
Michael Vogt Pending
Review via email: mp+251723@code.launchpad.net

Description of the change

* ubuntucoreupgrader/upgrader.py:
  - get_file_contents(): Fix to avoid leaking the fd.
* functional/test_upgrader.py: Basic set of functional tests.

To post a comment you must log in.
18. By James Hunt

* Fix-up changelog.

19. By James Hunt

* Moved common test code into ubuntucoreupgrader/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

Subscribers

People subscribed via source and target branches

to all changes: