blob: 4efb3bdafcb6328bb4c24172969554f1eb7789b9 [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))
62 def test_GetImageXBuddyRemote(self, _):
63 """Tests getting remote xBuddy image path."""
64 di = device_imager.DeviceImager(None, 'xbuddy://remote/eve/latest')
65 self.assertEqual(di._GetImage(),
66 ('gs://chromeos-image-archive/eve/R90',
67 device_imager.ImageType.REMOTE_DIRECTORY))
68
69 @mock.patch.object(xbuddy.XBuddy, 'Translate',
70 return_value=('eve/R90', 'path/to/file'))
71 def test_GetImageXBuddyLocal(self, _):
72 """Tests getting local xBuddy image path."""
73 di = device_imager.DeviceImager(None, 'xbuddy://local/eve/latest')
74 self.assertEqual(di._GetImage(),
75 ('path/to/file', device_imager.ImageType.FULL))
76
77 def test_SplitDevPath(self):
78 """Tests splitting a device path into prefix and partition number."""
79
80 di = device_imager.DeviceImager(None, None)
81 self.assertEqual(di._SplitDevPath('/dev/foop3'), ('/dev/foop', 3))
82
83 with self.assertRaises(device_imager.Error):
84 di._SplitDevPath('/foo')
85
86 with self.assertRaises(device_imager.Error):
87 di._SplitDevPath('/foo/p3p')
88
89 def test_GetKernelState(self):
90 """Tests getting the current active and inactive kernel states."""
91 di = device_imager.DeviceImager(None, None)
92 self.assertEqual(di._GetKernelState(3), (device_imager.DeviceImager.A,
93 device_imager.DeviceImager.B))
94 self.assertEqual(di._GetKernelState(5), (device_imager.DeviceImager.B,
95 device_imager.DeviceImager.A))
96
97 with self.assertRaises(device_imager.Error):
98 di._GetKernelState(1)
99
100 @mock.patch.object(remote_access.ChromiumOSDevice, 'root_dev',
101 return_value='/dev/foop3', new_callable=mock.PropertyMock)
102 def test_VerifyBootExpectations(self, _):
103 """Tests verifying the boot expectations after reboot."""
104
105 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
106 di = device_imager.DeviceImager(device, None)
107 di._inactive_state = device_imager.DeviceImager.A
108 di._VerifyBootExpectations()
109
110 @mock.patch.object(remote_access.ChromiumOSDevice, 'root_dev',
111 return_value='/dev/foop3', new_callable=mock.PropertyMock)
112 def test_VerifyBootExpectationsFails(self, _):
113 """Tests failure of boot expectations."""
114
115 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
116 di = device_imager.DeviceImager(device, None)
117 di._inactive_state = device_imager.DeviceImager.B
118 with self.assertRaises(device_imager.Error):
119 di._VerifyBootExpectations()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800120
121
122class TestReaderBase(cros_test_lib.MockTestCase):
123 """Test ReaderBase class"""
124
125 def testNamedPipe(self):
126 """Tests initializing the class with named pipe."""
127 with device_imager.ReaderBase(use_named_pipes=True) as r:
128 self.assertIsInstance(r.Target(), str)
129 self.assertEqual(r._Source(), r.Target())
130 self.assertExists(r.Target())
131
132 r._CloseSource() # Should not have any effect.
133 self.assertExists(r._Source())
134
135 # Closing target should delete the named pipe.
136 r.CloseTarget()
137 self.assertNotExists(r.Target())
138
139 def testFdPipe(self):
140 """Tests initializing the class with normal file descriptor pipes."""
141 with device_imager.ReaderBase() as r:
142 self.assertIsInstance(r.Target(), int)
143 self.assertIsInstance(r._Source(), int)
144 self.assertNotEqual(r._Source(), r.Target())
145 self.assertExists(GetFdPath(r.Target()))
146 self.assertExists(GetFdPath(r._Source()))
147
148 r._CloseSource()
149 self.assertNotExists(GetFdPath(r._Source()))
150 self.assertExists(GetFdPath(r.Target()))
151
152 r.CloseTarget()
153 self.assertNotExists(GetFdPath(r.Target()))
154
155 def testFdPipeCommunicate(self):
156 """Tests that file descriptors pipe can actually communicate."""
157 with device_imager.ReaderBase() as r:
158 with os.fdopen(r._Source(), 'w') as fp:
159 fp.write('helloworld')
160
161 with os.fdopen(r.Target(), 'r') as fp:
162 self.assertEqual(fp.read(), 'helloworld')
163
164
165class PartialFileReaderTest(cros_test_lib.RunCommandTestCase):
166 """Tests PartialFileReader class."""
167
168 def testRun(self):
169 """Tests the main run() function."""
170 with device_imager.PartialFileReader(
171 '/foo', 512 * 2, 512, cros_build_lib.COMP_GZIP) as pfr:
172 pass
173
174 self.assertCommandCalled(
175 'dd status=none if=/foo ibs=512 skip=2 count=1 | /usr/bin/pigz',
176 stdout=pfr._Source(), shell=True)
177
178 # Make sure the source has been close.
179 self.assertNotExists(GetFdPath(pfr._Source()))
180
181
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800182class GsFileCopierTest(cros_test_lib.TestCase):
183 """Tests GsFileCopier class."""
184
185 @mock.patch.object(gs.GSContext, 'Copy')
186 def testRun(self, copy_mock):
187 """Tests the run() function."""
188 image = 'gs://path/to/image'
189 with device_imager.GsFileCopier(image) as gfc:
190 self.assertTrue(gfc._use_named_pipes)
191
192 copy_mock.assert_called_with(image, gfc._Source())
193
194
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800195class PartitionUpdaterBaseTest(cros_test_lib.TestCase):
196 """Tests PartitionUpdaterBase class"""
197
198 def testRunNotImplemented(self):
199 """Tests running the main Run() function is not implemented."""
200 # We just want to make sure the _Run() function is not implemented here.
201 pub = device_imager.PartitionUpdaterBase(None, None, None, None, None)
202 with self.assertRaises(NotImplementedError):
203 pub.Run()
204
205 def testRevertNotImplemented(self):
206 """Tests running the Revert() function is not implemented."""
207 pub = device_imager.PartitionUpdaterBase(None, None, None, None, None)
208 with self.assertRaises(NotImplementedError):
209 pub.Revert()
210
211 @mock.patch.object(device_imager.PartitionUpdaterBase, '_Run')
212 def testIsFinished(self, _):
213 """Tests IsFinished() function."""
214 pub = device_imager.PartitionUpdaterBase(None, None, None, None, None)
215 self.assertFalse(pub.IsFinished())
216 pub.Run()
217 self.assertTrue(pub.IsFinished())
218
219
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800220class RawPartitionUpdaterTest(cros_test_lib.MockTempDirTestCase):
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800221 """Tests RawPartitionUpdater class."""
222
223 def setUp(self):
224 """Sets up the class by creating proper mocks."""
225 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
226 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
227 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
228
229 @mock.patch.object(device_imager.RawPartitionUpdater, '_GetPartitionName',
230 return_value=constants.PART_KERN_A)
231 @mock.patch.object(image_lib, 'GetImageDiskPartitionInfo',
232 return_value=image_lib_unittest.LOOP_PARTITION_INFO)
233 @mock.patch.object(device_imager.PartialFileReader, 'CloseTarget')
234 @mock.patch.object(device_imager.PartialFileReader, 'run')
235 def test_RunFullImage(self, run_mock, close_mock, _, name_mock):
236 """Test main Run() function for full image.
237
238 This function should parts of the source image and write it into the device
239 using proper compression programs.
240 """
241 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
242 self.rsh_mock.AddCmdResult([partial_mock.In('which'), 'gzip'],
243 returncode=0)
244 self.rsh_mock.AddCmdResult(
245 self.path_env +
246 ' gzip --decompress --stdout | dd bs=1M oflag=direct of=/dev/mmcblk0p2')
247
248 device_imager.RawPartitionUpdater(
249 device, 'foo-image', device_imager.ImageType.FULL,
250 '/dev/mmcblk0p2', cros_build_lib.COMP_GZIP).Run()
251 run_mock.assert_called()
252 close_mock.assert_called()
253 name_mock.assert_called()
Amin Hassanid684e982021-02-26 11:10:58 -0800254
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800255 def test_RunRemoteImage(self):
256 """Test main Run() function for remote images."""
257 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
258 self.rsh_mock.AddCmdResult([partial_mock.In('which'), 'gzip'],
259 returncode=0)
260 self.rsh_mock.AddCmdResult(
261 self.path_env +
262 ' gzip --decompress --stdout | dd bs=1M oflag=direct of=/dev/mmcblk0p2')
263
264 path = os.path.join(self.tempdir,
265 constants.QUICK_PROVISION_PAYLOAD_KERNEL)
266 with open(path, 'w') as image:
267 image.write('helloworld')
268
269 device_imager.KernelUpdater(
270 device, self.tempdir, device_imager.ImageType.REMOTE_DIRECTORY,
271 '/dev/mmcblk0p2', cros_build_lib.COMP_GZIP).Run()
272
Amin Hassanid684e982021-02-26 11:10:58 -0800273
274class KernelUpdaterTest(cros_test_lib.MockTempDirTestCase):
275 """Tests KernelUpdater class."""
276
277 def test_GetPartitionName(self):
278 """Tests the name of the partitions."""
279 ku = device_imager.KernelUpdater(None, None, None, None, None)
280 self.assertEqual(constants.PART_KERN_B, ku._GetPartitionName())
Amin Hassani75c5f942021-02-20 23:56:53 -0800281
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800282 def test_GetRemotePartitionName(self):
283 """Tests the name of the partitions."""
284 ku = device_imager.KernelUpdater(None, None, None, None, None)
285 self.assertEqual(constants.QUICK_PROVISION_PAYLOAD_KERNEL,
286 ku._GetRemotePartitionName())
287
Amin Hassani75c5f942021-02-20 23:56:53 -0800288
289class RootfsUpdaterTest(cros_test_lib.MockTestCase):
290 """Tests RootfsUpdater class."""
291
292 def setUp(self):
293 """Sets up the class by creating proper mocks."""
294 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
295 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
296 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
297
298 def test_GetPartitionName(self):
299 """Tests the name of the partitions."""
300 ru = device_imager.RootfsUpdater(None, None, None, None, None, None)
301 self.assertEqual(constants.PART_ROOT_A, ru._GetPartitionName())
302
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800303 def test_GetRemotePartitionName(self):
304 """Tests the name of the partitions."""
305 ru = device_imager.RootfsUpdater(None, None, None, None, None, None)
306 self.assertEqual(constants.QUICK_PROVISION_PAYLOAD_ROOTFS,
307 ru._GetRemotePartitionName())
308
Amin Hassani55970562021-02-22 20:49:13 -0800309 @mock.patch.object(device_imager.ProgressWatcher, 'run')
Amin Hassani75c5f942021-02-20 23:56:53 -0800310 @mock.patch.object(device_imager.RootfsUpdater, '_RunPostInst')
311 @mock.patch.object(device_imager.RootfsUpdater, '_CopyPartitionFromImage')
Amin Hassani55970562021-02-22 20:49:13 -0800312 def test_Run(self, copy_mock, postinst_mock, pw_mock):
Amin Hassani75c5f942021-02-20 23:56:53 -0800313 """Test main Run() function.
314
315 This function should parts of the source image and write it into the device
316 using proper compression programs.
317 """
318 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
319 device_imager.RootfsUpdater(
320 '/dev/mmcblk0p5', device, 'foo-image', device_imager.ImageType.FULL,
321 '/dev/mmcblk0p3', cros_build_lib.COMP_GZIP).Run()
322
323 copy_mock.assert_called_with(constants.PART_ROOT_A)
324 postinst_mock.assert_called_with()
Amin Hassani55970562021-02-22 20:49:13 -0800325 pw_mock.assert_called()
Amin Hassani75c5f942021-02-20 23:56:53 -0800326
327 def test_RunPostInstOnTarget(self):
328 """Test _RunPostInst() function."""
329 target = '/dev/mmcblk0p3'
330 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
331 device._work_dir = '/tmp/work_dir'
332 temp_dir = os.path.join(device.work_dir, 'dir')
333 self.rsh_mock.AddCmdResult(
334 [self.path_env, 'mktemp', '-d', '-p', device.work_dir],
335 stdout=temp_dir)
336 self.rsh_mock.AddCmdResult(
337 [self.path_env, 'mount', '-o', 'ro', target, temp_dir])
338 self.rsh_mock.AddCmdResult(
339 [self.path_env, os.path.join(temp_dir, 'postinst'), target])
340 self.rsh_mock.AddCmdResult([self.path_env, 'umount', temp_dir])
341
342 device_imager.RootfsUpdater(
343 '/dev/mmcblk0p5', device, 'foo-image', device_imager.ImageType.FULL,
344 target, cros_build_lib.COMP_GZIP)._RunPostInst()
345
346 def test_RunPostInstOnCurrentRoot(self):
347 """Test _RunPostInst() on current root (used for reverting an update)."""
348 root_dev = '/dev/mmcblk0p5'
349 self.rsh_mock.AddCmdResult([self.path_env, '/postinst', root_dev])
350
351 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
352 device_imager.RootfsUpdater(
353 root_dev, device, 'foo-image', device_imager.ImageType.FULL,
354 '/dev/mmcblk0p3', cros_build_lib.COMP_GZIP)._RunPostInst(
355 on_target=False)
356
357 @mock.patch.object(device_imager.RootfsUpdater, '_RunPostInst')
358 def testRevert(self, postinst_mock):
359 """Tests Revert() function."""
360 ru = device_imager.RootfsUpdater(None, None, None, None, None, None)
361
362 ru.Revert()
363 postinst_mock.assert_not_called()
364
365 ru._ran_postinst = True
366 ru.Revert()
367 postinst_mock.assert_called_with(on_target=False)
Amin Hassani74403082021-02-22 11:40:09 -0800368
369
370class StatefulPayloadGeneratorTest(cros_test_lib.TestCase):
371 """Tests stateful payload generator."""
372 @mock.patch.object(paygen_stateful_payload_lib, 'GenerateStatefulPayload')
373 def testRun(self, paygen_mock):
374 """Tests run() function."""
375 image = '/foo/image'
376 with device_imager.StatefulPayloadGenerator(image) as spg:
377 pass
378
379 paygen_mock.assert_called_with(image, spg._Source())
380
381
382class StatefulUpdaterTest(cros_test_lib.TestCase):
383 """Tests StatefulUpdater."""
384 @mock.patch.object(paygen_stateful_payload_lib, 'GenerateStatefulPayload')
385 @mock.patch.object(stateful_updater.StatefulUpdater, 'Update')
386 def test_RunFullImage(self, update_mock, paygen_mock):
387 """Test main Run() function for full image."""
388 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
389 device_imager.StatefulUpdater(False, device, 'foo-image',
390 device_imager.ImageType.FULL, None, None).Run()
391 update_mock.assert_called_with(mock.ANY,
392 is_payload_on_device=False,
393 update_type=None)
394 paygen_mock.assert_called()
395
396 @mock.patch.object(gs.GSContext, 'Copy')
397 @mock.patch.object(stateful_updater.StatefulUpdater, 'Update')
398 def test_RunRemoteImage(self, update_mock, copy_mock):
399 """Test main Run() function for remote images."""
400 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
401 device_imager.StatefulUpdater(False, device, 'gs://foo-image',
402 device_imager.ImageType.REMOTE_DIRECTORY, None,
403 None).Run()
404 copy_mock.assert_called_with('gs://foo-image/stateful.tgz',mock.ANY)
405 update_mock.assert_called_with(mock.ANY, is_payload_on_device=False,
406 update_type=None)
407
408 @mock.patch.object(stateful_updater.StatefulUpdater, 'Reset')
409 def testRevert(self, reset_mock):
410 """Tests Revert() function."""
411 su = device_imager.StatefulUpdater(False, None, None, None, None, None)
412
413 su.Revert()
414 reset_mock.assert_called()
Amin Hassani55970562021-02-22 20:49:13 -0800415
416
417class ProgressWatcherTest(cros_test_lib.MockTestCase):
418 """Tests ProgressWatcher class"""
419
420 def setUp(self):
421 """Sets up the class by creating proper mocks."""
422 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
423 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
424 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
425
426 @mock.patch.object(time, 'sleep')
427 @mock.patch.object(device_imager.ProgressWatcher, '_ShouldExit',
428 side_effect=[False, False, True])
429 # pylint: disable=unused-argument
430 def testRun(self, exit_mock, _):
431 """Tests the run() function."""
432 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
433 target_root = '/foo/root'
434 self.rsh_mock.AddCmdResult(
435 [self.path_env, 'blockdev', '--getsize64', target_root], stdout='100')
436 self.rsh_mock.AddCmdResult(
437 self.path_env + f' lsof 2>/dev/null | grep {target_root}',
438 stdout='xz 999')
439 self.rsh_mock.AddCmdResult([self.path_env, 'cat', '/proc/999/fdinfo/1'],
440 stdout='pos: 10\nflags: foo')
441 pw = device_imager.ProgressWatcher(device, target_root)
442 pw.run()