blob: 38a056824dc742a75075d81519abbb2b7601f14a [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 tempfile
Amin Hassani55970562021-02-22 20:49:13 -08009import time
Mike Frysinger166fea02021-02-12 05:30:33 -050010from unittest import mock
Amin Hassani92f6c4a2021-02-20 17:36:09 -080011
Amin Hassani92f6c4a2021-02-20 17:36:09 -080012from chromite.cli import device_imager
Amin Hassanid4b3ff82021-02-20 23:05:14 -080013from chromite.lib import constants
14from chromite.lib import cros_build_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080015from chromite.lib import cros_test_lib
Amin Hassani0fe49ae2021-02-21 23:41:58 -080016from chromite.lib import gs
Amin Hassanid4b3ff82021-02-20 23:05:14 -080017from chromite.lib import image_lib
18from chromite.lib import image_lib_unittest
Amin Hassani92f6c4a2021-02-20 17:36:09 -080019from chromite.lib import partial_mock
20from chromite.lib import remote_access
21from chromite.lib import remote_access_unittest
Amin Hassani74403082021-02-22 11:40:09 -080022from chromite.lib import stateful_updater
23from chromite.lib.paygen import paygen_stateful_payload_lib
Amin Hassani92f6c4a2021-02-20 17:36:09 -080024from chromite.lib.xbuddy import xbuddy
25
26
Amin Hassani92f6c4a2021-02-20 17:36:09 -080027# pylint: disable=protected-access
28
Amin Hassanid4b3ff82021-02-20 23:05:14 -080029
30def GetFdPath(fd):
31 """Returns the fd path for the current process."""
32 return f'/proc/self/fd/{fd}'
33
34
Amin Hassani92f6c4a2021-02-20 17:36:09 -080035class DeviceImagerTest(cros_test_lib.MockTestCase):
36 """Tests DeviceImager class methods."""
37
38 def setUp(self):
39 """Sets up the class by creating proper mocks."""
40 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
41 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
42 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
43
Amin Hassanib1993eb2021-04-28 12:00:11 -070044 def test_LocateImageLocalFile(self):
Amin Hassani92f6c4a2021-02-20 17:36:09 -080045 """Tests getting the path to local image."""
46 with tempfile.NamedTemporaryFile() as fp:
47 di = device_imager.DeviceImager(None, fp.name)
Amin Hassanib1993eb2021-04-28 12:00:11 -070048 di._LocateImage()
49 self.assertEqual(di._image, fp.name)
50 self.assertEqual(di._image_type, device_imager.ImageType.FULL)
Amin Hassani92f6c4a2021-02-20 17:36:09 -080051
Amin Hassanib1993eb2021-04-28 12:00:11 -070052 def test_LocateImageDir(self):
Amin Hassani92f6c4a2021-02-20 17:36:09 -080053 """Tests failing on a given directory as a path."""
54 di = device_imager.DeviceImager(None, '/tmp')
55 with self.assertRaises(ValueError):
Amin Hassanib1993eb2021-04-28 12:00:11 -070056 di._LocateImage()
Amin Hassani92f6c4a2021-02-20 17:36:09 -080057
58 @mock.patch.object(xbuddy.XBuddy, 'Translate', return_value=('eve/R90', None))
Amin Hassani70c372a2021-03-31 20:18:51 -070059 @mock.patch.object(remote_access.ChromiumOSDevice, 'board',
60 return_value='foo', new_callable=mock.PropertyMock)
61 # pylint: disable=unused-argument
Amin Hassanib1993eb2021-04-28 12:00:11 -070062 def test_LocateImageXBuddyRemote(self, _, board_mock):
Amin Hassani92f6c4a2021-02-20 17:36:09 -080063 """Tests getting remote xBuddy image path."""
Amin Hassani70c372a2021-03-31 20:18:51 -070064 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
65 di = device_imager.DeviceImager(device, 'xbuddy://remote/eve/latest')
Amin Hassanib1993eb2021-04-28 12:00:11 -070066 di._LocateImage()
67 self.assertEqual(di._image, 'gs://chromeos-image-archive/eve/R90')
68 self.assertEqual(di._image_type, device_imager.ImageType.REMOTE_DIRECTORY)
Amin Hassani92f6c4a2021-02-20 17:36:09 -080069
70 @mock.patch.object(xbuddy.XBuddy, 'Translate',
71 return_value=('eve/R90', 'path/to/file'))
Amin Hassani70c372a2021-03-31 20:18:51 -070072 @mock.patch.object(remote_access.ChromiumOSDevice, 'board',
73 return_value='foo', new_callable=mock.PropertyMock)
74 # pylint: disable=unused-argument
Amin Hassanib1993eb2021-04-28 12:00:11 -070075 def test_LocateImageXBuddyLocal(self, _, board_mock):
Amin Hassani92f6c4a2021-02-20 17:36:09 -080076 """Tests getting local xBuddy image path."""
Amin Hassani70c372a2021-03-31 20:18:51 -070077 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
78 di = device_imager.DeviceImager(device, 'xbuddy://local/eve/latest')
Amin Hassanib1993eb2021-04-28 12:00:11 -070079 di._LocateImage()
80 self.assertEqual(di._image, 'path/to/file')
81 self.assertEqual(di._image_type, device_imager.ImageType.FULL)
Amin Hassani92f6c4a2021-02-20 17:36:09 -080082
83 def test_SplitDevPath(self):
84 """Tests splitting a device path into prefix and partition number."""
85
86 di = device_imager.DeviceImager(None, None)
87 self.assertEqual(di._SplitDevPath('/dev/foop3'), ('/dev/foop', 3))
88
89 with self.assertRaises(device_imager.Error):
90 di._SplitDevPath('/foo')
91
92 with self.assertRaises(device_imager.Error):
93 di._SplitDevPath('/foo/p3p')
94
95 def test_GetKernelState(self):
96 """Tests getting the current active and inactive kernel states."""
97 di = device_imager.DeviceImager(None, None)
98 self.assertEqual(di._GetKernelState(3), (device_imager.DeviceImager.A,
99 device_imager.DeviceImager.B))
100 self.assertEqual(di._GetKernelState(5), (device_imager.DeviceImager.B,
101 device_imager.DeviceImager.A))
102
103 with self.assertRaises(device_imager.Error):
104 di._GetKernelState(1)
105
106 @mock.patch.object(remote_access.ChromiumOSDevice, 'root_dev',
107 return_value='/dev/foop3', new_callable=mock.PropertyMock)
108 def test_VerifyBootExpectations(self, _):
109 """Tests verifying the boot expectations after reboot."""
110
111 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
112 di = device_imager.DeviceImager(device, None)
113 di._inactive_state = device_imager.DeviceImager.A
114 di._VerifyBootExpectations()
115
116 @mock.patch.object(remote_access.ChromiumOSDevice, 'root_dev',
117 return_value='/dev/foop3', new_callable=mock.PropertyMock)
118 def test_VerifyBootExpectationsFails(self, _):
119 """Tests failure of boot expectations."""
120
121 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
122 di = device_imager.DeviceImager(device, None)
123 di._inactive_state = device_imager.DeviceImager.B
124 with self.assertRaises(device_imager.Error):
125 di._VerifyBootExpectations()
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800126
127
128class TestReaderBase(cros_test_lib.MockTestCase):
129 """Test ReaderBase class"""
130
131 def testNamedPipe(self):
132 """Tests initializing the class with named pipe."""
133 with device_imager.ReaderBase(use_named_pipes=True) as r:
134 self.assertIsInstance(r.Target(), str)
135 self.assertEqual(r._Source(), r.Target())
136 self.assertExists(r.Target())
137
138 r._CloseSource() # Should not have any effect.
139 self.assertExists(r._Source())
140
141 # Closing target should delete the named pipe.
142 r.CloseTarget()
143 self.assertNotExists(r.Target())
144
145 def testFdPipe(self):
146 """Tests initializing the class with normal file descriptor pipes."""
147 with device_imager.ReaderBase() as r:
148 self.assertIsInstance(r.Target(), int)
149 self.assertIsInstance(r._Source(), int)
150 self.assertNotEqual(r._Source(), r.Target())
151 self.assertExists(GetFdPath(r.Target()))
152 self.assertExists(GetFdPath(r._Source()))
153
Amin Hassanifa11c692021-04-07 09:17:31 -0700154 # Per crbug.com/1196702 it seems like some other process gets the file
155 # descriptor right after we close it and by the time we check its
156 # existence, it is still there and this can flake. So it might be better
157 # to make sure this is checked properly through real paths and not
158 # symlinks.
159 path = GetFdPath(r._Source())
160 old_path = os.path.realpath(path)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800161 r._CloseSource()
Amin Hassanifa11c692021-04-07 09:17:31 -0700162 with self.assertRaises(OSError):
163 new_path = os.path.realpath(path)
164 self.assertNotEqual(old_path, new_path)
165 raise OSError('Fake the context manager.')
166
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800167 self.assertExists(GetFdPath(r.Target()))
168
Amin Hassanifa11c692021-04-07 09:17:31 -0700169 path = GetFdPath(r.Target())
170 old_path = os.path.realpath(path)
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800171 r.CloseTarget()
Amin Hassanifa11c692021-04-07 09:17:31 -0700172 with self.assertRaises(OSError):
173 new_path = os.path.realpath(path)
174 self.assertNotEqual(old_path, new_path)
175 raise OSError('Fake the context manager.')
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800176
177 def testFdPipeCommunicate(self):
178 """Tests that file descriptors pipe can actually communicate."""
179 with device_imager.ReaderBase() as r:
180 with os.fdopen(r._Source(), 'w') as fp:
181 fp.write('helloworld')
182
183 with os.fdopen(r.Target(), 'r') as fp:
184 self.assertEqual(fp.read(), 'helloworld')
185
186
187class PartialFileReaderTest(cros_test_lib.RunCommandTestCase):
188 """Tests PartialFileReader class."""
189
190 def testRun(self):
191 """Tests the main run() function."""
192 with device_imager.PartialFileReader(
193 '/foo', 512 * 2, 512, cros_build_lib.COMP_GZIP) as pfr:
194 pass
195
196 self.assertCommandCalled(
197 'dd status=none if=/foo ibs=512 skip=2 count=1 | /usr/bin/pigz',
198 stdout=pfr._Source(), shell=True)
199
200 # Make sure the source has been close.
201 self.assertNotExists(GetFdPath(pfr._Source()))
202
203
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800204class GsFileCopierTest(cros_test_lib.TestCase):
205 """Tests GsFileCopier class."""
206
207 @mock.patch.object(gs.GSContext, 'Copy')
208 def testRun(self, copy_mock):
209 """Tests the run() function."""
210 image = 'gs://path/to/image'
211 with device_imager.GsFileCopier(image) as gfc:
212 self.assertTrue(gfc._use_named_pipes)
213
214 copy_mock.assert_called_with(image, gfc._Source())
215
216
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800217class PartitionUpdaterBaseTest(cros_test_lib.TestCase):
218 """Tests PartitionUpdaterBase class"""
219
220 def testRunNotImplemented(self):
221 """Tests running the main Run() function is not implemented."""
222 # We just want to make sure the _Run() function is not implemented here.
223 pub = device_imager.PartitionUpdaterBase(None, None, None, None, None)
224 with self.assertRaises(NotImplementedError):
225 pub.Run()
226
227 def testRevertNotImplemented(self):
228 """Tests running the Revert() function is not implemented."""
229 pub = device_imager.PartitionUpdaterBase(None, None, None, None, None)
230 with self.assertRaises(NotImplementedError):
231 pub.Revert()
232
233 @mock.patch.object(device_imager.PartitionUpdaterBase, '_Run')
234 def testIsFinished(self, _):
235 """Tests IsFinished() function."""
236 pub = device_imager.PartitionUpdaterBase(None, None, None, None, None)
237 self.assertFalse(pub.IsFinished())
238 pub.Run()
239 self.assertTrue(pub.IsFinished())
240
241
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800242class RawPartitionUpdaterTest(cros_test_lib.MockTempDirTestCase):
Amin Hassanid4b3ff82021-02-20 23:05:14 -0800243 """Tests RawPartitionUpdater class."""
244
245 def setUp(self):
246 """Sets up the class by creating proper mocks."""
247 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
248 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
249 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
250
251 @mock.patch.object(device_imager.RawPartitionUpdater, '_GetPartitionName',
252 return_value=constants.PART_KERN_A)
253 @mock.patch.object(image_lib, 'GetImageDiskPartitionInfo',
254 return_value=image_lib_unittest.LOOP_PARTITION_INFO)
255 @mock.patch.object(device_imager.PartialFileReader, 'CloseTarget')
256 @mock.patch.object(device_imager.PartialFileReader, 'run')
257 def test_RunFullImage(self, run_mock, close_mock, _, name_mock):
258 """Test main Run() function for full image.
259
260 This function should parts of the source image and write it into the device
261 using proper compression programs.
262 """
263 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
264 self.rsh_mock.AddCmdResult([partial_mock.In('which'), 'gzip'],
265 returncode=0)
266 self.rsh_mock.AddCmdResult(
267 self.path_env +
268 ' gzip --decompress --stdout | dd bs=1M oflag=direct of=/dev/mmcblk0p2')
269
270 device_imager.RawPartitionUpdater(
271 device, 'foo-image', device_imager.ImageType.FULL,
272 '/dev/mmcblk0p2', cros_build_lib.COMP_GZIP).Run()
273 run_mock.assert_called()
274 close_mock.assert_called()
275 name_mock.assert_called()
Amin Hassanid684e982021-02-26 11:10:58 -0800276
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800277 def test_RunRemoteImage(self):
278 """Test main Run() function for remote images."""
279 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
280 self.rsh_mock.AddCmdResult([partial_mock.In('which'), 'gzip'],
281 returncode=0)
282 self.rsh_mock.AddCmdResult(
283 self.path_env +
284 ' gzip --decompress --stdout | dd bs=1M oflag=direct of=/dev/mmcblk0p2')
285
286 path = os.path.join(self.tempdir,
287 constants.QUICK_PROVISION_PAYLOAD_KERNEL)
288 with open(path, 'w') as image:
289 image.write('helloworld')
290
291 device_imager.KernelUpdater(
292 device, self.tempdir, device_imager.ImageType.REMOTE_DIRECTORY,
293 '/dev/mmcblk0p2', cros_build_lib.COMP_GZIP).Run()
294
Amin Hassanid684e982021-02-26 11:10:58 -0800295
296class KernelUpdaterTest(cros_test_lib.MockTempDirTestCase):
297 """Tests KernelUpdater class."""
298
299 def test_GetPartitionName(self):
300 """Tests the name of the partitions."""
301 ku = device_imager.KernelUpdater(None, None, None, None, None)
302 self.assertEqual(constants.PART_KERN_B, ku._GetPartitionName())
Amin Hassani75c5f942021-02-20 23:56:53 -0800303
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800304 def test_GetRemotePartitionName(self):
305 """Tests the name of the partitions."""
306 ku = device_imager.KernelUpdater(None, None, None, None, None)
307 self.assertEqual(constants.QUICK_PROVISION_PAYLOAD_KERNEL,
308 ku._GetRemotePartitionName())
309
Amin Hassani75c5f942021-02-20 23:56:53 -0800310
311class RootfsUpdaterTest(cros_test_lib.MockTestCase):
312 """Tests RootfsUpdater class."""
313
314 def setUp(self):
315 """Sets up the class by creating proper mocks."""
316 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
317 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
318 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
319
320 def test_GetPartitionName(self):
321 """Tests the name of the partitions."""
322 ru = device_imager.RootfsUpdater(None, None, None, None, None, None)
323 self.assertEqual(constants.PART_ROOT_A, ru._GetPartitionName())
324
Amin Hassani0fe49ae2021-02-21 23:41:58 -0800325 def test_GetRemotePartitionName(self):
326 """Tests the name of the partitions."""
327 ru = device_imager.RootfsUpdater(None, None, None, None, None, None)
328 self.assertEqual(constants.QUICK_PROVISION_PAYLOAD_ROOTFS,
329 ru._GetRemotePartitionName())
330
Amin Hassani55970562021-02-22 20:49:13 -0800331 @mock.patch.object(device_imager.ProgressWatcher, 'run')
Amin Hassani75c5f942021-02-20 23:56:53 -0800332 @mock.patch.object(device_imager.RootfsUpdater, '_RunPostInst')
333 @mock.patch.object(device_imager.RootfsUpdater, '_CopyPartitionFromImage')
Amin Hassani55970562021-02-22 20:49:13 -0800334 def test_Run(self, copy_mock, postinst_mock, pw_mock):
Amin Hassani75c5f942021-02-20 23:56:53 -0800335 """Test main Run() function.
336
337 This function should parts of the source image and write it into the device
338 using proper compression programs.
339 """
340 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
341 device_imager.RootfsUpdater(
342 '/dev/mmcblk0p5', device, 'foo-image', device_imager.ImageType.FULL,
343 '/dev/mmcblk0p3', cros_build_lib.COMP_GZIP).Run()
344
345 copy_mock.assert_called_with(constants.PART_ROOT_A)
346 postinst_mock.assert_called_with()
Amin Hassani55970562021-02-22 20:49:13 -0800347 pw_mock.assert_called()
Amin Hassani75c5f942021-02-20 23:56:53 -0800348
349 def test_RunPostInstOnTarget(self):
350 """Test _RunPostInst() function."""
351 target = '/dev/mmcblk0p3'
352 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
353 device._work_dir = '/tmp/work_dir'
354 temp_dir = os.path.join(device.work_dir, 'dir')
355 self.rsh_mock.AddCmdResult(
356 [self.path_env, 'mktemp', '-d', '-p', device.work_dir],
357 stdout=temp_dir)
358 self.rsh_mock.AddCmdResult(
359 [self.path_env, 'mount', '-o', 'ro', target, temp_dir])
360 self.rsh_mock.AddCmdResult(
361 [self.path_env, os.path.join(temp_dir, 'postinst'), target])
362 self.rsh_mock.AddCmdResult([self.path_env, 'umount', temp_dir])
363
364 device_imager.RootfsUpdater(
365 '/dev/mmcblk0p5', device, 'foo-image', device_imager.ImageType.FULL,
366 target, cros_build_lib.COMP_GZIP)._RunPostInst()
367
368 def test_RunPostInstOnCurrentRoot(self):
369 """Test _RunPostInst() on current root (used for reverting an update)."""
370 root_dev = '/dev/mmcblk0p5'
371 self.rsh_mock.AddCmdResult([self.path_env, '/postinst', root_dev])
372
373 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
374 device_imager.RootfsUpdater(
375 root_dev, device, 'foo-image', device_imager.ImageType.FULL,
376 '/dev/mmcblk0p3', cros_build_lib.COMP_GZIP)._RunPostInst(
377 on_target=False)
378
379 @mock.patch.object(device_imager.RootfsUpdater, '_RunPostInst')
380 def testRevert(self, postinst_mock):
381 """Tests Revert() function."""
382 ru = device_imager.RootfsUpdater(None, None, None, None, None, None)
383
384 ru.Revert()
385 postinst_mock.assert_not_called()
386
387 ru._ran_postinst = True
388 ru.Revert()
389 postinst_mock.assert_called_with(on_target=False)
Amin Hassani74403082021-02-22 11:40:09 -0800390
391
392class StatefulPayloadGeneratorTest(cros_test_lib.TestCase):
393 """Tests stateful payload generator."""
394 @mock.patch.object(paygen_stateful_payload_lib, 'GenerateStatefulPayload')
395 def testRun(self, paygen_mock):
396 """Tests run() function."""
397 image = '/foo/image'
398 with device_imager.StatefulPayloadGenerator(image) as spg:
399 pass
400
401 paygen_mock.assert_called_with(image, spg._Source())
402
403
404class StatefulUpdaterTest(cros_test_lib.TestCase):
405 """Tests StatefulUpdater."""
406 @mock.patch.object(paygen_stateful_payload_lib, 'GenerateStatefulPayload')
407 @mock.patch.object(stateful_updater.StatefulUpdater, 'Update')
408 def test_RunFullImage(self, update_mock, paygen_mock):
409 """Test main Run() function for full image."""
410 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
411 device_imager.StatefulUpdater(False, device, 'foo-image',
412 device_imager.ImageType.FULL, None, None).Run()
413 update_mock.assert_called_with(mock.ANY,
414 is_payload_on_device=False,
415 update_type=None)
416 paygen_mock.assert_called()
417
418 @mock.patch.object(gs.GSContext, 'Copy')
419 @mock.patch.object(stateful_updater.StatefulUpdater, 'Update')
420 def test_RunRemoteImage(self, update_mock, copy_mock):
421 """Test main Run() function for remote images."""
422 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
423 device_imager.StatefulUpdater(False, device, 'gs://foo-image',
424 device_imager.ImageType.REMOTE_DIRECTORY, None,
425 None).Run()
426 copy_mock.assert_called_with('gs://foo-image/stateful.tgz',mock.ANY)
427 update_mock.assert_called_with(mock.ANY, is_payload_on_device=False,
428 update_type=None)
429
430 @mock.patch.object(stateful_updater.StatefulUpdater, 'Reset')
431 def testRevert(self, reset_mock):
432 """Tests Revert() function."""
433 su = device_imager.StatefulUpdater(False, None, None, None, None, None)
434
435 su.Revert()
436 reset_mock.assert_called()
Amin Hassani55970562021-02-22 20:49:13 -0800437
438
439class ProgressWatcherTest(cros_test_lib.MockTestCase):
440 """Tests ProgressWatcher class"""
441
442 def setUp(self):
443 """Sets up the class by creating proper mocks."""
444 self.rsh_mock = self.StartPatcher(remote_access_unittest.RemoteShMock())
445 self.rsh_mock.AddCmdResult(partial_mock.In('${PATH}'), stdout='')
446 self.path_env = 'PATH=%s:' % remote_access.DEV_BIN_PATHS
447
448 @mock.patch.object(time, 'sleep')
449 @mock.patch.object(device_imager.ProgressWatcher, '_ShouldExit',
450 side_effect=[False, False, True])
451 # pylint: disable=unused-argument
452 def testRun(self, exit_mock, _):
453 """Tests the run() function."""
454 with remote_access.ChromiumOSDeviceHandler(remote_access.TEST_IP) as device:
455 target_root = '/foo/root'
456 self.rsh_mock.AddCmdResult(
457 [self.path_env, 'blockdev', '--getsize64', target_root], stdout='100')
458 self.rsh_mock.AddCmdResult(
459 self.path_env + f' lsof 2>/dev/null | grep {target_root}',
460 stdout='xz 999')
461 self.rsh_mock.AddCmdResult([self.path_env, 'cat', '/proc/999/fdinfo/1'],
462 stdout='pos: 10\nflags: foo')
463 pw = device_imager.ProgressWatcher(device, target_root)
464 pw.run()