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