blob: afbee6f3f0399ef7634ec0ebe67bbf7770aeae1c [file] [log] [blame]
Amin Hassani92f6c4a2021-02-20 17:36:09 -08001# Copyright 2021 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Unit tests for the device_imager module."""
6
Amin Hassanid4b3ff82021-02-20 23:05:14 -08007import os
Amin Hassani92f6c4a2021-02-20 17:36:09 -08008import sys
9import tempfile
Amin Hassani55970562021-02-22 20:49:13 -080010import time
Amin Hassani92f6c4a2021-02-20 17:36:09 -080011
12import mock
13
14from chromite.cli import device_imager
Amin Hassanid4b3ff82021-02-20 23:05:14 -080015from chromite.lib import constants
16from chromite.lib import cros_build_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080017from chromite.lib import cros_test_lib
Amin Hassani0fe49ae2021-02-21 23:41:58 -080018from chromite.lib import gs
Amin Hassanid4b3ff82021-02-20 23:05:14 -080019from chromite.lib import image_lib
20from chromite.lib import image_lib_unittest
Amin Hassani92f6c4a2021-02-20 17:36:09 -080021from chromite.lib import partial_mock
22from chromite.lib import remote_access
23from chromite.lib import remote_access_unittest
Amin Hassani74403082021-02-22 11:40:09 -080024from chromite.lib import stateful_updater
25from chromite.lib.paygen import paygen_stateful_payload_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080026from chromite.lib.xbuddy import xbuddy
27
28
29assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
30
31
32# pylint: disable=protected-access
33
Amin Hassanid4b3ff82021-02-20 23:05:14 -080034
35def GetFdPath(fd):
36 """Returns the fd path for the current process."""
37 return f'/proc/self/fd/{fd}'
38
39
Amin Hassani92f6c4a2021-02-20 17:36:09 -080040class DeviceImagerTest(cros_test_lib.MockTestCase):
41 """Tests DeviceImager class methods."""
42
43 def setUp(self):
44 """Sets up the class by creating proper mocks."""
45 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
46 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
47 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
48
49 def test_GetImageLocalFile(self):
50 """Tests getting the path to local image."""
51 with tempfile.NamedTemporaryFile() as fp:
52 di = device_imager.DeviceImager(None, fp.name)
53 self.assertEqual(di._GetImage(), (fp.name, device_imager.ImageType.FULL))
54
55 def test_GetImageDir(self):
56 """Tests failing on a given directory as a path."""
57 di = device_imager.DeviceImager(None, '/tmp')
58 with self.assertRaises(ValueError):
59 di._GetImage()
60
61 @mock.patch.object(xbuddy.XBuddy, 'Translate', return_value=('eve/R90', None))
Amin Hassani70c372a2021-03-31 20:18:51 -070062 @mock.patch.object(remote_access.ChromiumOSDevice, 'board',
63 return_value='foo', new_callable=mock.PropertyMock)
64 # pylint: disable=unused-argument
65 def test_GetImageXBuddyRemote(self, _, board_mock):
Amin Hassani92f6c4a2021-02-20 17:36:09 -080066 """Tests getting remote xBuddy image path."""
Amin Hassani70c372a2021-03-31 20:18:51 -070067 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
68 di = device_imager.DeviceImager(device, 'xbuddy://remote/eve/latest')
69 self.assertEqual(di._GetImage(),
70 ('gs://chromeos-image-archive/eve/R90',
71 device_imager.ImageType.REMOTE_DIRECTORY))
Amin Hassani92f6c4a2021-02-20 17:36:09 -080072
73 @mock.patch.object(xbuddy.XBuddy, 'Translate',
74 return_value=('eve/R90', 'path/to/file'))
Amin Hassani70c372a2021-03-31 20:18:51 -070075 @mock.patch.object(remote_access.ChromiumOSDevice, 'board',
76 return_value='foo', new_callable=mock.PropertyMock)
77 # pylint: disable=unused-argument
78 def test_GetImageXBuddyLocal(self, _, board_mock):
Amin Hassani92f6c4a2021-02-20 17:36:09 -080079 """Tests getting local xBuddy image path."""
Amin Hassani70c372a2021-03-31 20:18:51 -070080 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
81 di = device_imager.DeviceImager(device, 'xbuddy://local/eve/latest')
82 self.assertEqual(di._GetImage(),
83 ('path/to/file', device_imager.ImageType.FULL))
Amin Hassani92f6c4a2021-02-20 17:36:09 -080084
85 def test_SplitDevPath(self):
86 """Tests splitting a device path into prefix and partition number."""
87
88 di = device_imager.DeviceImager(None, None)
89 self.assertEqual(di._SplitDevPath('/dev/foop3'), ('/dev/foop', 3))
90
91 with self.assertRaises(device_imager.Error):
92 di._SplitDevPath('/foo')
93
94 with self.assertRaises(device_imager.Error):
95 di._SplitDevPath('/foo/p3p')
96
97 def test_GetKernelState(self):
98 """Tests getting the current active and inactive kernel states."""
99 di = device_imager.DeviceImager(None, None)
100 self.assertEqual(di._GetKernelState(3), (device_imager.DeviceImager.A,
101 device_imager.DeviceImager.B))
102 self.assertEqual(di._GetKernelState(5), (device_imager.DeviceImager.B,
103 device_imager.DeviceImager.A))
104
105 with self.assertRaises(device_imager.Error):
106 di._GetKernelState(1)
107
108 @mock.patch.object(remote_access.ChromiumOSDevice, 'root_dev',
109 return_value='/dev/foop3', new_callable=mock.PropertyMock)
110 def test_VerifyBootExpectations(self, _):
111 """Tests verifying the boot expectations after reboot."""
112
113 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
114 di = device_imager.DeviceImager(device, None)
115 di._inactive_state = device_imager.DeviceImager.A
116 di._VerifyBootExpectations()
117
118 @mock.patch.object(remote_access.ChromiumOSDevice, 'root_dev',
119 return_value='/dev/foop3', new_callable=mock.PropertyMock)
120 def test_VerifyBootExpectationsFails(self, _):
121 """Tests failure of boot expectations."""
122
123 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
124 di = device_imager.DeviceImager(device, None)
125 di._inactive_state = device_imager.DeviceImager.B
126 with self.assertRaises(device_imager.Error):
127 di._VerifyBootExpectations()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800128
129
130class TestReaderBase(cros_test_lib.MockTestCase):
131 """Test ReaderBase class"""
132
133 def testNamedPipe(self):
134 """Tests initializing the class with named pipe."""
135 with device_imager.ReaderBase(use_named_pipes=True) as r:
136 self.assertIsInstance(r.Target(), str)
137 self.assertEqual(r._Source(), r.Target())
138 self.assertExists(r.Target())
139
140 r._CloseSource() # Should not have any effect.
141 self.assertExists(r._Source())
142
143 # Closing target should delete the named pipe.
144 r.CloseTarget()
145 self.assertNotExists(r.Target())
146
147 def testFdPipe(self):
148 """Tests initializing the class with normal file descriptor pipes."""
149 with device_imager.ReaderBase() as r:
150 self.assertIsInstance(r.Target(), int)
151 self.assertIsInstance(r._Source(), int)
152 self.assertNotEqual(r._Source(), r.Target())
153 self.assertExists(GetFdPath(r.Target()))
154 self.assertExists(GetFdPath(r._Source()))
155
Amin Hassanifa11c692021-04-07 09:17:31 -0700156 # Per crbug.com/1196702 it seems like some other process gets the file
157 # descriptor right after we close it and by the time we check its
158 # existence, it is still there and this can flake. So it might be better
159 # to make sure this is checked properly through real paths and not
160 # symlinks.
161 path = GetFdPath(r._Source())
162 old_path = os.path.realpath(path)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800163 r._CloseSource()
Amin Hassanifa11c692021-04-07 09:17:31 -0700164 with self.assertRaises(OSError):
165 new_path = os.path.realpath(path)
166 self.assertNotEqual(old_path, new_path)
167 raise OSError('Fake the context manager.')
168
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800169 self.assertExists(GetFdPath(r.Target()))
170
Amin Hassanifa11c692021-04-07 09:17:31 -0700171 path = GetFdPath(r.Target())
172 old_path = os.path.realpath(path)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800173 r.CloseTarget()
Amin Hassanifa11c692021-04-07 09:17:31 -0700174 with self.assertRaises(OSError):
175 new_path = os.path.realpath(path)
176 self.assertNotEqual(old_path, new_path)
177 raise OSError('Fake the context manager.')
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800178
179 def testFdPipeCommunicate(self):
180 """Tests that file descriptors pipe can actually communicate."""
181 with device_imager.ReaderBase() as r:
182 with os.fdopen(r._Source(), 'w') as fp:
183 fp.write('helloworld')
184
185 with os.fdopen(r.Target(), 'r') as fp:
186 self.assertEqual(fp.read(), 'helloworld')
187
188
189class PartialFileReaderTest(cros_test_lib.RunCommandTestCase):
190 """Tests PartialFileReader class."""
191
192 def testRun(self):
193 """Tests the main run() function."""
194 with device_imager.PartialFileReader(
195 '/foo', 512 * 2, 512, cros_build_lib.COMP_GZIP) as pfr:
196 pass
197
198 self.assertCommandCalled(
199 'dd status=none if=/foo ibs=512 skip=2 count=1 | /usr/bin/pigz',
200 stdout=pfr._Source(), shell=True)
201
202 # Make sure the source has been close.
203 self.assertNotExists(GetFdPath(pfr._Source()))
204
205
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800206class GsFileCopierTest(cros_test_lib.TestCase):
207 """Tests GsFileCopier class."""
208
209 @mock.patch.object(gs.GSContext, 'Copy')
210 def testRun(self, copy_mock):
211 """Tests the run() function."""
212 image = 'gs://path/to/image'
213 with device_imager.GsFileCopier(image) as gfc:
214 self.assertTrue(gfc._use_named_pipes)
215
216 copy_mock.assert_called_with(image, gfc._Source())
217
218
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800219class PartitionUpdaterBaseTest(cros_test_lib.TestCase):
220 """Tests PartitionUpdaterBase class"""
221
222 def testRunNotImplemented(self):
223 """Tests running the main Run() function is not implemented."""
224 # We just want to make sure the _Run() function is not implemented here.
225 pub = device_imager.PartitionUpdaterBase(None, None, None, None, None)
226 with self.assertRaises(NotImplementedError):
227 pub.Run()
228
229 def testRevertNotImplemented(self):
230 """Tests running the Revert() function is not implemented."""
231 pub = device_imager.PartitionUpdaterBase(None, None, None, None, None)
232 with self.assertRaises(NotImplementedError):
233 pub.Revert()
234
235 @mock.patch.object(device_imager.PartitionUpdaterBase, '_Run')
236 def testIsFinished(self, _):
237 """Tests IsFinished() function."""
238 pub = device_imager.PartitionUpdaterBase(None, None, None, None, None)
239 self.assertFalse(pub.IsFinished())
240 pub.Run()
241 self.assertTrue(pub.IsFinished())
242
243
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800244class RawPartitionUpdaterTest(cros_test_lib.MockTempDirTestCase):
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800245 """Tests RawPartitionUpdater class."""
246
247 def setUp(self):
248 """Sets up the class by creating proper mocks."""
249 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
250 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
251 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
252
253 @mock.patch.object(device_imager.RawPartitionUpdater, '_GetPartitionName',
254 return_value=constants.PART_KERN_A)
255 @mock.patch.object(image_lib, 'GetImageDiskPartitionInfo',
256 return_value=image_lib_unittest.LOOP_PARTITION_INFO)
257 @mock.patch.object(device_imager.PartialFileReader, 'CloseTarget')
258 @mock.patch.object(device_imager.PartialFileReader, 'run')
259 def test_RunFullImage(self, run_mock, close_mock, _, name_mock):
260 """Test main Run() function for full image.
261
262 This function should parts of the source image and write it into the device
263 using proper compression programs.
264 """
265 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
266 self.rsh_mock.AddCmdResult([partial_mock.In('which'), 'gzip'],
267 returncode=0)
268 self.rsh_mock.AddCmdResult(
269 self.path_env +
270 ' gzip --decompress --stdout | dd bs=1M oflag=direct of=/dev/mmcblk0p2')
271
272 device_imager.RawPartitionUpdater(
273 device, 'foo-image', device_imager.ImageType.FULL,
274 '/dev/mmcblk0p2', cros_build_lib.COMP_GZIP).Run()
275 run_mock.assert_called()
276 close_mock.assert_called()
277 name_mock.assert_called()
Amin Hassanid684e982021-02-26 11:10:58 -0800278
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800279 def test_RunRemoteImage(self):
280 """Test main Run() function for remote images."""
281 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
282 self.rsh_mock.AddCmdResult([partial_mock.In('which'), 'gzip'],
283 returncode=0)
284 self.rsh_mock.AddCmdResult(
285 self.path_env +
286 ' gzip --decompress --stdout | dd bs=1M oflag=direct of=/dev/mmcblk0p2')
287
288 path = os.path.join(self.tempdir,
289 constants.QUICK_PROVISION_PAYLOAD_KERNEL)
290 with open(path, 'w') as image:
291 image.write('helloworld')
292
293 device_imager.KernelUpdater(
294 device, self.tempdir, device_imager.ImageType.REMOTE_DIRECTORY,
295 '/dev/mmcblk0p2', cros_build_lib.COMP_GZIP).Run()
296
Amin Hassanid684e982021-02-26 11:10:58 -0800297
298class KernelUpdaterTest(cros_test_lib.MockTempDirTestCase):
299 """Tests KernelUpdater class."""
300
301 def test_GetPartitionName(self):
302 """Tests the name of the partitions."""
303 ku = device_imager.KernelUpdater(None, None, None, None, None)
304 self.assertEqual(constants.PART_KERN_B, ku._GetPartitionName())
Amin Hassani75c5f942021-02-20 23:56:53 -0800305
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800306 def test_GetRemotePartitionName(self):
307 """Tests the name of the partitions."""
308 ku = device_imager.KernelUpdater(None, None, None, None, None)
309 self.assertEqual(constants.QUICK_PROVISION_PAYLOAD_KERNEL,
310 ku._GetRemotePartitionName())
311
Amin Hassani75c5f942021-02-20 23:56:53 -0800312
313class RootfsUpdaterTest(cros_test_lib.MockTestCase):
314 """Tests RootfsUpdater class."""
315
316 def setUp(self):
317 """Sets up the class by creating proper mocks."""
318 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
319 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
320 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
321
322 def test_GetPartitionName(self):
323 """Tests the name of the partitions."""
324 ru = device_imager.RootfsUpdater(None, None, None, None, None, None)
325 self.assertEqual(constants.PART_ROOT_A, ru._GetPartitionName())
326
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800327 def test_GetRemotePartitionName(self):
328 """Tests the name of the partitions."""
329 ru = device_imager.RootfsUpdater(None, None, None, None, None, None)
330 self.assertEqual(constants.QUICK_PROVISION_PAYLOAD_ROOTFS,
331 ru._GetRemotePartitionName())
332
Amin Hassani55970562021-02-22 20:49:13 -0800333 @mock.patch.object(device_imager.ProgressWatcher, 'run')
Amin Hassani75c5f942021-02-20 23:56:53 -0800334 @mock.patch.object(device_imager.RootfsUpdater, '_RunPostInst')
335 @mock.patch.object(device_imager.RootfsUpdater, '_CopyPartitionFromImage')
Amin Hassani55970562021-02-22 20:49:13 -0800336 def test_Run(self, copy_mock, postinst_mock, pw_mock):
Amin Hassani75c5f942021-02-20 23:56:53 -0800337 """Test main Run() function.
338
339 This function should parts of the source image and write it into the device
340 using proper compression programs.
341 """
342 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
343 device_imager.RootfsUpdater(
344 '/dev/mmcblk0p5', device, 'foo-image', device_imager.ImageType.FULL,
345 '/dev/mmcblk0p3', cros_build_lib.COMP_GZIP).Run()
346
347 copy_mock.assert_called_with(constants.PART_ROOT_A)
348 postinst_mock.assert_called_with()
Amin Hassani55970562021-02-22 20:49:13 -0800349 pw_mock.assert_called()
Amin Hassani75c5f942021-02-20 23:56:53 -0800350
351 def test_RunPostInstOnTarget(self):
352 """Test _RunPostInst() function."""
353 target = '/dev/mmcblk0p3'
354 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
355 device._work_dir = '/tmp/work_dir'
356 temp_dir = os.path.join(device.work_dir, 'dir')
357 self.rsh_mock.AddCmdResult(
358 [self.path_env, 'mktemp', '-d', '-p', device.work_dir],
359 stdout=temp_dir)
360 self.rsh_mock.AddCmdResult(
361 [self.path_env, 'mount', '-o', 'ro', target, temp_dir])
362 self.rsh_mock.AddCmdResult(
363 [self.path_env, os.path.join(temp_dir, 'postinst'), target])
364 self.rsh_mock.AddCmdResult([self.path_env, 'umount', temp_dir])
365
366 device_imager.RootfsUpdater(
367 '/dev/mmcblk0p5', device, 'foo-image', device_imager.ImageType.FULL,
368 target, cros_build_lib.COMP_GZIP)._RunPostInst()
369
370 def test_RunPostInstOnCurrentRoot(self):
371 """Test _RunPostInst() on current root (used for reverting an update)."""
372 root_dev = '/dev/mmcblk0p5'
373 self.rsh_mock.AddCmdResult([self.path_env, '/postinst', root_dev])
374
375 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
376 device_imager.RootfsUpdater(
377 root_dev, device, 'foo-image', device_imager.ImageType.FULL,
378 '/dev/mmcblk0p3', cros_build_lib.COMP_GZIP)._RunPostInst(
379 on_target=False)
380
381 @mock.patch.object(device_imager.RootfsUpdater, '_RunPostInst')
382 def testRevert(self, postinst_mock):
383 """Tests Revert() function."""
384 ru = device_imager.RootfsUpdater(None, None, None, None, None, None)
385
386 ru.Revert()
387 postinst_mock.assert_not_called()
388
389 ru._ran_postinst = True
390 ru.Revert()
391 postinst_mock.assert_called_with(on_target=False)
Amin Hassani74403082021-02-22 11:40:09 -0800392
393
394class StatefulPayloadGeneratorTest(cros_test_lib.TestCase):
395 """Tests stateful payload generator."""
396 @mock.patch.object(paygen_stateful_payload_lib, 'GenerateStatefulPayload')
397 def testRun(self, paygen_mock):
398 """Tests run() function."""
399 image = '/foo/image'
400 with device_imager.StatefulPayloadGenerator(image) as spg:
401 pass
402
403 paygen_mock.assert_called_with(image, spg._Source())
404
405
406class StatefulUpdaterTest(cros_test_lib.TestCase):
407 """Tests StatefulUpdater."""
408 @mock.patch.object(paygen_stateful_payload_lib, 'GenerateStatefulPayload')
409 @mock.patch.object(stateful_updater.StatefulUpdater, 'Update')
410 def test_RunFullImage(self, update_mock, paygen_mock):
411 """Test main Run() function for full image."""
412 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
413 device_imager.StatefulUpdater(False, device, 'foo-image',
414 device_imager.ImageType.FULL, None, None).Run()
415 update_mock.assert_called_with(mock.ANY,
416 is_payload_on_device=False,
417 update_type=None)
418 paygen_mock.assert_called()
419
420 @mock.patch.object(gs.GSContext, 'Copy')
421 @mock.patch.object(stateful_updater.StatefulUpdater, 'Update')
422 def test_RunRemoteImage(self, update_mock, copy_mock):
423 """Test main Run() function for remote images."""
424 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
425 device_imager.StatefulUpdater(False, device, 'gs://foo-image',
426 device_imager.ImageType.REMOTE_DIRECTORY, None,
427 None).Run()
428 copy_mock.assert_called_with('gs://foo-image/stateful.tgz',mock.ANY)
429 update_mock.assert_called_with(mock.ANY, is_payload_on_device=False,
430 update_type=None)
431
432 @mock.patch.object(stateful_updater.StatefulUpdater, 'Reset')
433 def testRevert(self, reset_mock):
434 """Tests Revert() function."""
435 su = device_imager.StatefulUpdater(False, None, None, None, None, None)
436
437 su.Revert()
438 reset_mock.assert_called()
Amin Hassani55970562021-02-22 20:49:13 -0800439
440
441class ProgressWatcherTest(cros_test_lib.MockTestCase):
442 """Tests ProgressWatcher class"""
443
444 def setUp(self):
445 """Sets up the class by creating proper mocks."""
446 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
447 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
448 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
449
450 @mock.patch.object(time, 'sleep')
451 @mock.patch.object(device_imager.ProgressWatcher, '_ShouldExit',
452 side_effect=[False, False, True])
453 # pylint: disable=unused-argument
454 def testRun(self, exit_mock, _):
455 """Tests the run() function."""
456 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
457 target_root = '/foo/root'
458 self.rsh_mock.AddCmdResult(
459 [self.path_env, 'blockdev', '--getsize64', target_root], stdout='100')
460 self.rsh_mock.AddCmdResult(
461 self.path_env + f' lsof 2>/dev/null | grep {target_root}',
462 stdout='xz 999')
463 self.rsh_mock.AddCmdResult([self.path_env, 'cat', '/proc/999/fdinfo/1'],
464 stdout='pos: 10\nflags: foo')
465 pw = device_imager.ProgressWatcher(device, target_root)
466 pw.run()