blob: 5311089df36a400c5e038c187975bed2b95dd109 [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
156 r._CloseSource()
157 self.assertNotExists(GetFdPath(r._Source()))
158 self.assertExists(GetFdPath(r.Target()))
159
160 r.CloseTarget()
161 self.assertNotExists(GetFdPath(r.Target()))
162
163 def testFdPipeCommunicate(self):
164 """Tests that file descriptors pipe can actually communicate."""
165 with device_imager.ReaderBase() as r:
166 with os.fdopen(r._Source(), 'w') as fp:
167 fp.write('helloworld')
168
169 with os.fdopen(r.Target(), 'r') as fp:
170 self.assertEqual(fp.read(), 'helloworld')
171
172
173class PartialFileReaderTest(cros_test_lib.RunCommandTestCase):
174 """Tests PartialFileReader class."""
175
176 def testRun(self):
177 """Tests the main run() function."""
178 with device_imager.PartialFileReader(
179 '/foo', 512 * 2, 512, cros_build_lib.COMP_GZIP) as pfr:
180 pass
181
182 self.assertCommandCalled(
183 'dd status=none if=/foo ibs=512 skip=2 count=1 | /usr/bin/pigz',
184 stdout=pfr._Source(), shell=True)
185
186 # Make sure the source has been close.
187 self.assertNotExists(GetFdPath(pfr._Source()))
188
189
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800190class GsFileCopierTest(cros_test_lib.TestCase):
191 """Tests GsFileCopier class."""
192
193 @mock.patch.object(gs.GSContext, 'Copy')
194 def testRun(self, copy_mock):
195 """Tests the run() function."""
196 image = 'gs://path/to/image'
197 with device_imager.GsFileCopier(image) as gfc:
198 self.assertTrue(gfc._use_named_pipes)
199
200 copy_mock.assert_called_with(image, gfc._Source())
201
202
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800203class PartitionUpdaterBaseTest(cros_test_lib.TestCase):
204 """Tests PartitionUpdaterBase class"""
205
206 def testRunNotImplemented(self):
207 """Tests running the main Run() function is not implemented."""
208 # We just want to make sure the _Run() function is not implemented here.
209 pub = device_imager.PartitionUpdaterBase(None, None, None, None, None)
210 with self.assertRaises(NotImplementedError):
211 pub.Run()
212
213 def testRevertNotImplemented(self):
214 """Tests running the Revert() function is not implemented."""
215 pub = device_imager.PartitionUpdaterBase(None, None, None, None, None)
216 with self.assertRaises(NotImplementedError):
217 pub.Revert()
218
219 @mock.patch.object(device_imager.PartitionUpdaterBase, '_Run')
220 def testIsFinished(self, _):
221 """Tests IsFinished() function."""
222 pub = device_imager.PartitionUpdaterBase(None, None, None, None, None)
223 self.assertFalse(pub.IsFinished())
224 pub.Run()
225 self.assertTrue(pub.IsFinished())
226
227
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800228class RawPartitionUpdaterTest(cros_test_lib.MockTempDirTestCase):
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800229 """Tests RawPartitionUpdater class."""
230
231 def setUp(self):
232 """Sets up the class by creating proper mocks."""
233 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
234 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
235 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
236
237 @mock.patch.object(device_imager.RawPartitionUpdater, '_GetPartitionName',
238 return_value=constants.PART_KERN_A)
239 @mock.patch.object(image_lib, 'GetImageDiskPartitionInfo',
240 return_value=image_lib_unittest.LOOP_PARTITION_INFO)
241 @mock.patch.object(device_imager.PartialFileReader, 'CloseTarget')
242 @mock.patch.object(device_imager.PartialFileReader, 'run')
243 def test_RunFullImage(self, run_mock, close_mock, _, name_mock):
244 """Test main Run() function for full image.
245
246 This function should parts of the source image and write it into the device
247 using proper compression programs.
248 """
249 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
250 self.rsh_mock.AddCmdResult([partial_mock.In('which'), 'gzip'],
251 returncode=0)
252 self.rsh_mock.AddCmdResult(
253 self.path_env +
254 ' gzip --decompress --stdout | dd bs=1M oflag=direct of=/dev/mmcblk0p2')
255
256 device_imager.RawPartitionUpdater(
257 device, 'foo-image', device_imager.ImageType.FULL,
258 '/dev/mmcblk0p2', cros_build_lib.COMP_GZIP).Run()
259 run_mock.assert_called()
260 close_mock.assert_called()
261 name_mock.assert_called()
Amin Hassanid684e982021-02-26 11:10:58 -0800262
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800263 def test_RunRemoteImage(self):
264 """Test main Run() function for remote images."""
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 path = os.path.join(self.tempdir,
273 constants.QUICK_PROVISION_PAYLOAD_KERNEL)
274 with open(path, 'w') as image:
275 image.write('helloworld')
276
277 device_imager.KernelUpdater(
278 device, self.tempdir, device_imager.ImageType.REMOTE_DIRECTORY,
279 '/dev/mmcblk0p2', cros_build_lib.COMP_GZIP).Run()
280
Amin Hassanid684e982021-02-26 11:10:58 -0800281
282class KernelUpdaterTest(cros_test_lib.MockTempDirTestCase):
283 """Tests KernelUpdater class."""
284
285 def test_GetPartitionName(self):
286 """Tests the name of the partitions."""
287 ku = device_imager.KernelUpdater(None, None, None, None, None)
288 self.assertEqual(constants.PART_KERN_B, ku._GetPartitionName())
Amin Hassani75c5f942021-02-20 23:56:53 -0800289
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800290 def test_GetRemotePartitionName(self):
291 """Tests the name of the partitions."""
292 ku = device_imager.KernelUpdater(None, None, None, None, None)
293 self.assertEqual(constants.QUICK_PROVISION_PAYLOAD_KERNEL,
294 ku._GetRemotePartitionName())
295
Amin Hassani75c5f942021-02-20 23:56:53 -0800296
297class RootfsUpdaterTest(cros_test_lib.MockTestCase):
298 """Tests RootfsUpdater class."""
299
300 def setUp(self):
301 """Sets up the class by creating proper mocks."""
302 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
303 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
304 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
305
306 def test_GetPartitionName(self):
307 """Tests the name of the partitions."""
308 ru = device_imager.RootfsUpdater(None, None, None, None, None, None)
309 self.assertEqual(constants.PART_ROOT_A, ru._GetPartitionName())
310
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800311 def test_GetRemotePartitionName(self):
312 """Tests the name of the partitions."""
313 ru = device_imager.RootfsUpdater(None, None, None, None, None, None)
314 self.assertEqual(constants.QUICK_PROVISION_PAYLOAD_ROOTFS,
315 ru._GetRemotePartitionName())
316
Amin Hassani55970562021-02-22 20:49:13 -0800317 @mock.patch.object(device_imager.ProgressWatcher, 'run')
Amin Hassani75c5f942021-02-20 23:56:53 -0800318 @mock.patch.object(device_imager.RootfsUpdater, '_RunPostInst')
319 @mock.patch.object(device_imager.RootfsUpdater, '_CopyPartitionFromImage')
Amin Hassani55970562021-02-22 20:49:13 -0800320 def test_Run(self, copy_mock, postinst_mock, pw_mock):
Amin Hassani75c5f942021-02-20 23:56:53 -0800321 """Test main Run() function.
322
323 This function should parts of the source image and write it into the device
324 using proper compression programs.
325 """
326 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
327 device_imager.RootfsUpdater(
328 '/dev/mmcblk0p5', device, 'foo-image', device_imager.ImageType.FULL,
329 '/dev/mmcblk0p3', cros_build_lib.COMP_GZIP).Run()
330
331 copy_mock.assert_called_with(constants.PART_ROOT_A)
332 postinst_mock.assert_called_with()
Amin Hassani55970562021-02-22 20:49:13 -0800333 pw_mock.assert_called()
Amin Hassani75c5f942021-02-20 23:56:53 -0800334
335 def test_RunPostInstOnTarget(self):
336 """Test _RunPostInst() function."""
337 target = '/dev/mmcblk0p3'
338 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
339 device._work_dir = '/tmp/work_dir'
340 temp_dir = os.path.join(device.work_dir, 'dir')
341 self.rsh_mock.AddCmdResult(
342 [self.path_env, 'mktemp', '-d', '-p', device.work_dir],
343 stdout=temp_dir)
344 self.rsh_mock.AddCmdResult(
345 [self.path_env, 'mount', '-o', 'ro', target, temp_dir])
346 self.rsh_mock.AddCmdResult(
347 [self.path_env, os.path.join(temp_dir, 'postinst'), target])
348 self.rsh_mock.AddCmdResult([self.path_env, 'umount', temp_dir])
349
350 device_imager.RootfsUpdater(
351 '/dev/mmcblk0p5', device, 'foo-image', device_imager.ImageType.FULL,
352 target, cros_build_lib.COMP_GZIP)._RunPostInst()
353
354 def test_RunPostInstOnCurrentRoot(self):
355 """Test _RunPostInst() on current root (used for reverting an update)."""
356 root_dev = '/dev/mmcblk0p5'
357 self.rsh_mock.AddCmdResult([self.path_env, '/postinst', root_dev])
358
359 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
360 device_imager.RootfsUpdater(
361 root_dev, device, 'foo-image', device_imager.ImageType.FULL,
362 '/dev/mmcblk0p3', cros_build_lib.COMP_GZIP)._RunPostInst(
363 on_target=False)
364
365 @mock.patch.object(device_imager.RootfsUpdater, '_RunPostInst')
366 def testRevert(self, postinst_mock):
367 """Tests Revert() function."""
368 ru = device_imager.RootfsUpdater(None, None, None, None, None, None)
369
370 ru.Revert()
371 postinst_mock.assert_not_called()
372
373 ru._ran_postinst = True
374 ru.Revert()
375 postinst_mock.assert_called_with(on_target=False)
Amin Hassani74403082021-02-22 11:40:09 -0800376
377
378class StatefulPayloadGeneratorTest(cros_test_lib.TestCase):
379 """Tests stateful payload generator."""
380 @mock.patch.object(paygen_stateful_payload_lib, 'GenerateStatefulPayload')
381 def testRun(self, paygen_mock):
382 """Tests run() function."""
383 image = '/foo/image'
384 with device_imager.StatefulPayloadGenerator(image) as spg:
385 pass
386
387 paygen_mock.assert_called_with(image, spg._Source())
388
389
390class StatefulUpdaterTest(cros_test_lib.TestCase):
391 """Tests StatefulUpdater."""
392 @mock.patch.object(paygen_stateful_payload_lib, 'GenerateStatefulPayload')
393 @mock.patch.object(stateful_updater.StatefulUpdater, 'Update')
394 def test_RunFullImage(self, update_mock, paygen_mock):
395 """Test main Run() function for full image."""
396 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
397 device_imager.StatefulUpdater(False, device, 'foo-image',
398 device_imager.ImageType.FULL, None, None).Run()
399 update_mock.assert_called_with(mock.ANY,
400 is_payload_on_device=False,
401 update_type=None)
402 paygen_mock.assert_called()
403
404 @mock.patch.object(gs.GSContext, 'Copy')
405 @mock.patch.object(stateful_updater.StatefulUpdater, 'Update')
406 def test_RunRemoteImage(self, update_mock, copy_mock):
407 """Test main Run() function for remote images."""
408 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
409 device_imager.StatefulUpdater(False, device, 'gs://foo-image',
410 device_imager.ImageType.REMOTE_DIRECTORY, None,
411 None).Run()
412 copy_mock.assert_called_with('gs://foo-image/stateful.tgz',mock.ANY)
413 update_mock.assert_called_with(mock.ANY, is_payload_on_device=False,
414 update_type=None)
415
416 @mock.patch.object(stateful_updater.StatefulUpdater, 'Reset')
417 def testRevert(self, reset_mock):
418 """Tests Revert() function."""
419 su = device_imager.StatefulUpdater(False, None, None, None, None, None)
420
421 su.Revert()
422 reset_mock.assert_called()
Amin Hassani55970562021-02-22 20:49:13 -0800423
424
425class ProgressWatcherTest(cros_test_lib.MockTestCase):
426 """Tests ProgressWatcher class"""
427
428 def setUp(self):
429 """Sets up the class by creating proper mocks."""
430 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
431 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
432 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
433
434 @mock.patch.object(time, 'sleep')
435 @mock.patch.object(device_imager.ProgressWatcher, '_ShouldExit',
436 side_effect=[False, False, True])
437 # pylint: disable=unused-argument
438 def testRun(self, exit_mock, _):
439 """Tests the run() function."""
440 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
441 target_root = '/foo/root'
442 self.rsh_mock.AddCmdResult(
443 [self.path_env, 'blockdev', '--getsize64', target_root], stdout='100')
444 self.rsh_mock.AddCmdResult(
445 self.path_env + f' lsof 2>/dev/null | grep {target_root}',
446 stdout='xz 999')
447 self.rsh_mock.AddCmdResult([self.path_env, 'cat', '/proc/999/fdinfo/1'],
448 stdout='pos: 10\nflags: foo')
449 pw = device_imager.ProgressWatcher(device, target_root)
450 pw.run()