blob: cfd11ec94c440d11d0684d1dd21522928bbaceff [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
10
11import mock
12
13from chromite.cli import device_imager
Amin Hassanid4b3ff82021-02-20 23:05:14 -080014from chromite.lib import constants
15from chromite.lib import cros_build_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080016from chromite.lib import cros_test_lib
Amin Hassani0fe49ae2021-02-21 23:41:58 -080017from chromite.lib import gs
Amin Hassanid4b3ff82021-02-20 23:05:14 -080018from chromite.lib import image_lib
19from chromite.lib import image_lib_unittest
Amin Hassani92f6c4a2021-02-20 17:36:09 -080020from chromite.lib import partial_mock
21from chromite.lib import remote_access
22from chromite.lib import remote_access_unittest
Amin Hassani74403082021-02-22 11:40:09 -080023from chromite.lib import stateful_updater
24from chromite.lib.paygen import paygen_stateful_payload_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080025from chromite.lib.xbuddy import xbuddy
26
27
28assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
29
30
31# pylint: disable=protected-access
32
Amin Hassanid4b3ff82021-02-20 23:05:14 -080033
34def GetFdPath(fd):
35 """Returns the fd path for the current process."""
36 return f'/proc/self/fd/{fd}'
37
38
Amin Hassani92f6c4a2021-02-20 17:36:09 -080039class DeviceImagerTest(cros_test_lib.MockTestCase):
40 """Tests DeviceImager class methods."""
41
42 def setUp(self):
43 """Sets up the class by creating proper mocks."""
44 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
45 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
46 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
47
48 def test_GetImageLocalFile(self):
49 """Tests getting the path to local image."""
50 with tempfile.NamedTemporaryFile() as fp:
51 di = device_imager.DeviceImager(None, fp.name)
52 self.assertEqual(di._GetImage(), (fp.name, device_imager.ImageType.FULL))
53
54 def test_GetImageDir(self):
55 """Tests failing on a given directory as a path."""
56 di = device_imager.DeviceImager(None, '/tmp')
57 with self.assertRaises(ValueError):
58 di._GetImage()
59
60 @mock.patch.object(xbuddy.XBuddy, 'Translate', return_value=('eve/R90', None))
61 def test_GetImageXBuddyRemote(self, _):
62 """Tests getting remote xBuddy image path."""
63 di = device_imager.DeviceImager(None, 'xbuddy://remote/eve/latest')
64 self.assertEqual(di._GetImage(),
65 ('gs://chromeos-image-archive/eve/R90',
66 device_imager.ImageType.REMOTE_DIRECTORY))
67
68 @mock.patch.object(xbuddy.XBuddy, 'Translate',
69 return_value=('eve/R90', 'path/to/file'))
70 def test_GetImageXBuddyLocal(self, _):
71 """Tests getting local xBuddy image path."""
72 di = device_imager.DeviceImager(None, 'xbuddy://local/eve/latest')
73 self.assertEqual(di._GetImage(),
74 ('path/to/file', device_imager.ImageType.FULL))
75
76 def test_SplitDevPath(self):
77 """Tests splitting a device path into prefix and partition number."""
78
79 di = device_imager.DeviceImager(None, None)
80 self.assertEqual(di._SplitDevPath('/dev/foop3'), ('/dev/foop', 3))
81
82 with self.assertRaises(device_imager.Error):
83 di._SplitDevPath('/foo')
84
85 with self.assertRaises(device_imager.Error):
86 di._SplitDevPath('/foo/p3p')
87
88 def test_GetKernelState(self):
89 """Tests getting the current active and inactive kernel states."""
90 di = device_imager.DeviceImager(None, None)
91 self.assertEqual(di._GetKernelState(3), (device_imager.DeviceImager.A,
92 device_imager.DeviceImager.B))
93 self.assertEqual(di._GetKernelState(5), (device_imager.DeviceImager.B,
94 device_imager.DeviceImager.A))
95
96 with self.assertRaises(device_imager.Error):
97 di._GetKernelState(1)
98
99 @mock.patch.object(remote_access.ChromiumOSDevice, 'root_dev',
100 return_value='/dev/foop3', new_callable=mock.PropertyMock)
101 def test_VerifyBootExpectations(self, _):
102 """Tests verifying the boot expectations after reboot."""
103
104 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
105 di = device_imager.DeviceImager(device, None)
106 di._inactive_state = device_imager.DeviceImager.A
107 di._VerifyBootExpectations()
108
109 @mock.patch.object(remote_access.ChromiumOSDevice, 'root_dev',
110 return_value='/dev/foop3', new_callable=mock.PropertyMock)
111 def test_VerifyBootExpectationsFails(self, _):
112 """Tests failure of boot expectations."""
113
114 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
115 di = device_imager.DeviceImager(device, None)
116 di._inactive_state = device_imager.DeviceImager.B
117 with self.assertRaises(device_imager.Error):
118 di._VerifyBootExpectations()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800119
120
121class TestReaderBase(cros_test_lib.MockTestCase):
122 """Test ReaderBase class"""
123
124 def testNamedPipe(self):
125 """Tests initializing the class with named pipe."""
126 with device_imager.ReaderBase(use_named_pipes=True) as r:
127 self.assertIsInstance(r.Target(), str)
128 self.assertEqual(r._Source(), r.Target())
129 self.assertExists(r.Target())
130
131 r._CloseSource() # Should not have any effect.
132 self.assertExists(r._Source())
133
134 # Closing target should delete the named pipe.
135 r.CloseTarget()
136 self.assertNotExists(r.Target())
137
138 def testFdPipe(self):
139 """Tests initializing the class with normal file descriptor pipes."""
140 with device_imager.ReaderBase() as r:
141 self.assertIsInstance(r.Target(), int)
142 self.assertIsInstance(r._Source(), int)
143 self.assertNotEqual(r._Source(), r.Target())
144 self.assertExists(GetFdPath(r.Target()))
145 self.assertExists(GetFdPath(r._Source()))
146
147 r._CloseSource()
148 self.assertNotExists(GetFdPath(r._Source()))
149 self.assertExists(GetFdPath(r.Target()))
150
151 r.CloseTarget()
152 self.assertNotExists(GetFdPath(r.Target()))
153
154 def testFdPipeCommunicate(self):
155 """Tests that file descriptors pipe can actually communicate."""
156 with device_imager.ReaderBase() as r:
157 with os.fdopen(r._Source(), 'w') as fp:
158 fp.write('helloworld')
159
160 with os.fdopen(r.Target(), 'r') as fp:
161 self.assertEqual(fp.read(), 'helloworld')
162
163
164class PartialFileReaderTest(cros_test_lib.RunCommandTestCase):
165 """Tests PartialFileReader class."""
166
167 def testRun(self):
168 """Tests the main run() function."""
169 with device_imager.PartialFileReader(
170 '/foo', 512 * 2, 512, cros_build_lib.COMP_GZIP) as pfr:
171 pass
172
173 self.assertCommandCalled(
174 'dd status=none if=/foo ibs=512 skip=2 count=1 | /usr/bin/pigz',
175 stdout=pfr._Source(), shell=True)
176
177 # Make sure the source has been close.
178 self.assertNotExists(GetFdPath(pfr._Source()))
179
180
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800181class GsFileCopierTest(cros_test_lib.TestCase):
182 """Tests GsFileCopier class."""
183
184 @mock.patch.object(gs.GSContext, 'Copy')
185 def testRun(self, copy_mock):
186 """Tests the run() function."""
187 image = 'gs://path/to/image'
188 with device_imager.GsFileCopier(image) as gfc:
189 self.assertTrue(gfc._use_named_pipes)
190
191 copy_mock.assert_called_with(image, gfc._Source())
192
193
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800194class PartitionUpdaterBaseTest(cros_test_lib.TestCase):
195 """Tests PartitionUpdaterBase class"""
196
197 def testRunNotImplemented(self):
198 """Tests running the main Run() function is not implemented."""
199 # We just want to make sure the _Run() function is not implemented here.
200 pub = device_imager.PartitionUpdaterBase(None, None, None, None, None)
201 with self.assertRaises(NotImplementedError):
202 pub.Run()
203
204 def testRevertNotImplemented(self):
205 """Tests running the Revert() function is not implemented."""
206 pub = device_imager.PartitionUpdaterBase(None, None, None, None, None)
207 with self.assertRaises(NotImplementedError):
208 pub.Revert()
209
210 @mock.patch.object(device_imager.PartitionUpdaterBase, '_Run')
211 def testIsFinished(self, _):
212 """Tests IsFinished() function."""
213 pub = device_imager.PartitionUpdaterBase(None, None, None, None, None)
214 self.assertFalse(pub.IsFinished())
215 pub.Run()
216 self.assertTrue(pub.IsFinished())
217
218
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800219class RawPartitionUpdaterTest(cros_test_lib.MockTempDirTestCase):
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800220 """Tests RawPartitionUpdater class."""
221
222 def setUp(self):
223 """Sets up the class by creating proper mocks."""
224 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
225 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
226 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
227
228 @mock.patch.object(device_imager.RawPartitionUpdater, '_GetPartitionName',
229 return_value=constants.PART_KERN_A)
230 @mock.patch.object(image_lib, 'GetImageDiskPartitionInfo',
231 return_value=image_lib_unittest.LOOP_PARTITION_INFO)
232 @mock.patch.object(device_imager.PartialFileReader, 'CloseTarget')
233 @mock.patch.object(device_imager.PartialFileReader, 'run')
234 def test_RunFullImage(self, run_mock, close_mock, _, name_mock):
235 """Test main Run() function for full image.
236
237 This function should parts of the source image and write it into the device
238 using proper compression programs.
239 """
240 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
241 self.rsh_mock.AddCmdResult([partial_mock.In('which'), 'gzip'],
242 returncode=0)
243 self.rsh_mock.AddCmdResult(
244 self.path_env +
245 ' gzip --decompress --stdout | dd bs=1M oflag=direct of=/dev/mmcblk0p2')
246
247 device_imager.RawPartitionUpdater(
248 device, 'foo-image', device_imager.ImageType.FULL,
249 '/dev/mmcblk0p2', cros_build_lib.COMP_GZIP).Run()
250 run_mock.assert_called()
251 close_mock.assert_called()
252 name_mock.assert_called()
Amin Hassanid684e982021-02-26 11:10:58 -0800253
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800254 def test_RunRemoteImage(self):
255 """Test main Run() function for remote images."""
256 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
257 self.rsh_mock.AddCmdResult([partial_mock.In('which'), 'gzip'],
258 returncode=0)
259 self.rsh_mock.AddCmdResult(
260 self.path_env +
261 ' gzip --decompress --stdout | dd bs=1M oflag=direct of=/dev/mmcblk0p2')
262
263 path = os.path.join(self.tempdir,
264 constants.QUICK_PROVISION_PAYLOAD_KERNEL)
265 with open(path, 'w') as image:
266 image.write('helloworld')
267
268 device_imager.KernelUpdater(
269 device, self.tempdir, device_imager.ImageType.REMOTE_DIRECTORY,
270 '/dev/mmcblk0p2', cros_build_lib.COMP_GZIP).Run()
271
Amin Hassanid684e982021-02-26 11:10:58 -0800272
273class KernelUpdaterTest(cros_test_lib.MockTempDirTestCase):
274 """Tests KernelUpdater class."""
275
276 def test_GetPartitionName(self):
277 """Tests the name of the partitions."""
278 ku = device_imager.KernelUpdater(None, None, None, None, None)
279 self.assertEqual(constants.PART_KERN_B, ku._GetPartitionName())
Amin Hassani75c5f942021-02-20 23:56:53 -0800280
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800281 def test_GetRemotePartitionName(self):
282 """Tests the name of the partitions."""
283 ku = device_imager.KernelUpdater(None, None, None, None, None)
284 self.assertEqual(constants.QUICK_PROVISION_PAYLOAD_KERNEL,
285 ku._GetRemotePartitionName())
286
Amin Hassani75c5f942021-02-20 23:56:53 -0800287
288class RootfsUpdaterTest(cros_test_lib.MockTestCase):
289 """Tests RootfsUpdater class."""
290
291 def setUp(self):
292 """Sets up the class by creating proper mocks."""
293 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
294 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
295 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
296
297 def test_GetPartitionName(self):
298 """Tests the name of the partitions."""
299 ru = device_imager.RootfsUpdater(None, None, None, None, None, None)
300 self.assertEqual(constants.PART_ROOT_A, ru._GetPartitionName())
301
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800302 def test_GetRemotePartitionName(self):
303 """Tests the name of the partitions."""
304 ru = device_imager.RootfsUpdater(None, None, None, None, None, None)
305 self.assertEqual(constants.QUICK_PROVISION_PAYLOAD_ROOTFS,
306 ru._GetRemotePartitionName())
307
Amin Hassani75c5f942021-02-20 23:56:53 -0800308 @mock.patch.object(device_imager.RootfsUpdater, '_RunPostInst')
309 @mock.patch.object(device_imager.RootfsUpdater, '_CopyPartitionFromImage')
310 def test_Run(self, copy_mock, postinst_mock):
311 """Test main Run() function.
312
313 This function should parts of the source image and write it into the device
314 using proper compression programs.
315 """
316 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
317 device_imager.RootfsUpdater(
318 '/dev/mmcblk0p5', device, 'foo-image', device_imager.ImageType.FULL,
319 '/dev/mmcblk0p3', cros_build_lib.COMP_GZIP).Run()
320
321 copy_mock.assert_called_with(constants.PART_ROOT_A)
322 postinst_mock.assert_called_with()
323
324 def test_RunPostInstOnTarget(self):
325 """Test _RunPostInst() function."""
326 target = '/dev/mmcblk0p3'
327 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
328 device._work_dir = '/tmp/work_dir'
329 temp_dir = os.path.join(device.work_dir, 'dir')
330 self.rsh_mock.AddCmdResult(
331 [self.path_env, 'mktemp', '-d', '-p', device.work_dir],
332 stdout=temp_dir)
333 self.rsh_mock.AddCmdResult(
334 [self.path_env, 'mount', '-o', 'ro', target, temp_dir])
335 self.rsh_mock.AddCmdResult(
336 [self.path_env, os.path.join(temp_dir, 'postinst'), target])
337 self.rsh_mock.AddCmdResult([self.path_env, 'umount', temp_dir])
338
339 device_imager.RootfsUpdater(
340 '/dev/mmcblk0p5', device, 'foo-image', device_imager.ImageType.FULL,
341 target, cros_build_lib.COMP_GZIP)._RunPostInst()
342
343 def test_RunPostInstOnCurrentRoot(self):
344 """Test _RunPostInst() on current root (used for reverting an update)."""
345 root_dev = '/dev/mmcblk0p5'
346 self.rsh_mock.AddCmdResult([self.path_env, '/postinst', root_dev])
347
348 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
349 device_imager.RootfsUpdater(
350 root_dev, device, 'foo-image', device_imager.ImageType.FULL,
351 '/dev/mmcblk0p3', cros_build_lib.COMP_GZIP)._RunPostInst(
352 on_target=False)
353
354 @mock.patch.object(device_imager.RootfsUpdater, '_RunPostInst')
355 def testRevert(self, postinst_mock):
356 """Tests Revert() function."""
357 ru = device_imager.RootfsUpdater(None, None, None, None, None, None)
358
359 ru.Revert()
360 postinst_mock.assert_not_called()
361
362 ru._ran_postinst = True
363 ru.Revert()
364 postinst_mock.assert_called_with(on_target=False)
Amin Hassani74403082021-02-22 11:40:09 -0800365
366
367class StatefulPayloadGeneratorTest(cros_test_lib.TestCase):
368 """Tests stateful payload generator."""
369 @mock.patch.object(paygen_stateful_payload_lib, 'GenerateStatefulPayload')
370 def testRun(self, paygen_mock):
371 """Tests run() function."""
372 image = '/foo/image'
373 with device_imager.StatefulPayloadGenerator(image) as spg:
374 pass
375
376 paygen_mock.assert_called_with(image, spg._Source())
377
378
379class StatefulUpdaterTest(cros_test_lib.TestCase):
380 """Tests StatefulUpdater."""
381 @mock.patch.object(paygen_stateful_payload_lib, 'GenerateStatefulPayload')
382 @mock.patch.object(stateful_updater.StatefulUpdater, 'Update')
383 def test_RunFullImage(self, update_mock, paygen_mock):
384 """Test main Run() function for full image."""
385 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
386 device_imager.StatefulUpdater(False, device, 'foo-image',
387 device_imager.ImageType.FULL, None, None).Run()
388 update_mock.assert_called_with(mock.ANY,
389 is_payload_on_device=False,
390 update_type=None)
391 paygen_mock.assert_called()
392
393 @mock.patch.object(gs.GSContext, 'Copy')
394 @mock.patch.object(stateful_updater.StatefulUpdater, 'Update')
395 def test_RunRemoteImage(self, update_mock, copy_mock):
396 """Test main Run() function for remote images."""
397 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
398 device_imager.StatefulUpdater(False, device, 'gs://foo-image',
399 device_imager.ImageType.REMOTE_DIRECTORY, None,
400 None).Run()
401 copy_mock.assert_called_with('gs://foo-image/stateful.tgz',mock.ANY)
402 update_mock.assert_called_with(mock.ANY, is_payload_on_device=False,
403 update_type=None)
404
405 @mock.patch.object(stateful_updater.StatefulUpdater, 'Reset')
406 def testRevert(self, reset_mock):
407 """Tests Revert() function."""
408 su = device_imager.StatefulUpdater(False, None, None, None, None, None)
409
410 su.Revert()
411 reset_mock.assert_called()