blob: c30cf755351d0f62b6247e2e71f371ca1e79352a [file] [log] [blame]
Tom Wai-Hong Tama70f0fe2011-09-02 18:28:47 +08001# Copyright (c) 2011 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
5import logging
Tom Wai-Hong Tam76c75072011-10-25 18:00:12 +08006import os
Tom Wai-Hong Tama70f0fe2011-09-02 18:28:47 +08007import re
Tom Wai-Hong Tam76c75072011-10-25 18:00:12 +08008import tempfile
Tom Wai-Hong Tama70f0fe2011-09-02 18:28:47 +08009import time
10import xmlrpclib
11
Tom Wai-Hong Tam76c75072011-10-25 18:00:12 +080012from autotest_lib.client.bin import utils
Tom Wai-Hong Tama70f0fe2011-09-02 18:28:47 +080013from autotest_lib.client.common_lib import error
Tom Wai-Hong Tam22b77302011-11-03 13:03:48 +080014from autotest_lib.server.cros.servo_test import ServoTest
Tom Wai-Hong Tama70f0fe2011-09-02 18:28:47 +080015
16
17class FAFTSequence(ServoTest):
18 """
19 The base class of Fully Automated Firmware Test Sequence.
20
21 Many firmware tests require several reboot cycles and verify the resulted
22 system states. To do that, an Autotest test case should detailly handle
23 every action on each step. It makes the test case hard to read and many
24 duplicated code. The base class FAFTSequence is to solve this problem.
25
26 The actions of one reboot cycle is defined in a dict, namely FAFT_STEP.
27 There are four functions in the FAFT_STEP dict:
28 state_checker: a function to check the current is valid or not,
29 returning True if valid, otherwise, False to break the whole
30 test sequence.
31 userspace_action: a function to describe the action ran in userspace.
Tom Wai-Hong Tamf1e34972011-11-02 17:07:04 +080032 reboot_action: a function to do reboot, default: sync_and_hw_reboot.
Tom Wai-Hong Tama70f0fe2011-09-02 18:28:47 +080033 firmware_action: a function to describe the action ran after reboot.
34
Tom Wai-Hong Tam7c17ff22011-10-26 09:44:09 +080035 And configurations:
36 install_deps_after_boot: if True, install the Autotest dependency after
37 boot; otherwise, do nothing. It is for the cases of recovery mode
38 test. The test boots a USB/SD image instead of an internal image.
39 The previous installed Autotest dependency on the internal image
40 is lost. So need to install it again.
41
Tom Wai-Hong Tama70f0fe2011-09-02 18:28:47 +080042 The default FAFT_STEP checks nothing in state_checker and does nothing in
Tom Wai-Hong Tamf1e34972011-11-02 17:07:04 +080043 userspace_action and firmware_action. Its reboot_action is a hardware
44 reboot. You can change the default FAFT_STEP by calling
Tom Wai-Hong Tama70f0fe2011-09-02 18:28:47 +080045 self.register_faft_template(FAFT_STEP).
46
47 A FAFT test case consists of several FAFT_STEP's, namely FAFT_SEQUENCE.
48 FAFT_SEQUENCE is an array of FAFT_STEP's. Any missing fields on FAFT_STEP
49 fall back to default.
50
51 In the run_once(), it should register and run FAFT_SEQUENCE like:
52 def run_once(self):
53 self.register_faft_sequence(FAFT_SEQUENCE)
54 self.run_faft_sequnce()
55
56 Note that in the last step, we only run state_checker. The
57 userspace_action, reboot_action, and firmware_action are not executed.
58
59 Attributes:
60 _faft_template: The default FAFT_STEP of each step. The actions would
61 be over-written if the registered FAFT_SEQUENCE is valid.
62 _faft_sequence: The registered FAFT_SEQUENCE.
63 """
64 version = 1
65
Tom Wai-Hong Tamcfda61f2011-11-02 17:41:01 +080066
67 # Mapping of partition number of kernel and rootfs.
68 KERNEL_MAP = {'a':'2', 'b':'4', '2':'2', '4':'4', '3':'2', '5':'4'}
69 ROOTFS_MAP = {'a':'3', 'b':'5', '2':'3', '4':'5', '3':'3', '5':'5'}
70 OTHER_KERNEL_MAP = {'a':'4', 'b':'2', '2':'4', '4':'2', '3':'4', '5':'2'}
71 OTHER_ROOTFS_MAP = {'a':'5', 'b':'3', '2':'5', '4':'3', '3':'5', '5':'3'}
72
Tom Wai-Hong Tama9c1a502011-11-10 06:39:26 +080073 # Delay timing
74 FIRMWARE_SCREEN_DELAY = 10
75 TEXT_SCREEN_DELAY = 20
Tom Wai-Hong Tam9ca742a2011-12-05 15:48:57 +080076 USB_PLUG_DELAY = 10
Tom Wai-Hong Tam6a863ba2011-12-08 10:13:28 +080077 SYNC_DELAY = 5
Tom Wai-Hong Tamcfda61f2011-11-02 17:41:01 +080078
Tom Wai-Hong Tama70f0fe2011-09-02 18:28:47 +080079 _faft_template = None
80 _faft_sequence = ()
81
82
83 def setup(self):
84 """Autotest setup function."""
85 super(FAFTSequence, self).setup()
86 if not self._remote_infos['faft']['used']:
87 raise error.TestError('The use_faft flag should be enabled.')
88 self.register_faft_template({
89 'state_checker': (None),
90 'userspace_action': (None),
Tom Wai-Hong Tamf1e34972011-11-02 17:07:04 +080091 'reboot_action': (self.sync_and_hw_reboot),
Tom Wai-Hong Tama70f0fe2011-09-02 18:28:47 +080092 'firmware_action': (None)
93 })
94
95
96 def cleanup(self):
97 """Autotest cleanup function."""
98 self._faft_sequence = ()
99 self._faft_template = None
100 super(FAFTSequence, self).cleanup()
101
102
Tom Wai-Hong Tam76c75072011-10-25 18:00:12 +0800103 def assert_test_image_in_usb_disk(self):
104 """Assert an USB disk plugged-in on servo and a test image inside.
105
106 Raises:
Tom Wai-Hong Tama9c1a502011-11-10 06:39:26 +0800107 error.TestError: if USB disk not detected or not a test image.
Tom Wai-Hong Tam76c75072011-10-25 18:00:12 +0800108 """
109 self.servo.set('usb_mux_sel1', 'servo_sees_usbkey')
Jon Salzc88e5b62011-11-30 14:38:54 +0800110 usb_dev = self.servo.probe_host_usb_dev()
Tom Wai-Hong Tam76c75072011-10-25 18:00:12 +0800111 if not usb_dev:
112 raise error.TestError(
113 'An USB disk should be plugged in the servo board.')
114
115 tmp_dir = tempfile.mkdtemp()
Tom Wai-Hong Tamb0e80852011-12-07 16:15:06 +0800116 utils.system('sudo mount -r -t ext2 %s3 %s' % (usb_dev, tmp_dir))
Tom Wai-Hong Tame77459e2011-11-03 17:19:46 +0800117 code = utils.system(
118 'grep -qE "(Test Build|testimage-channel)" %s/etc/lsb-release' %
119 tmp_dir, ignore_status=True)
Tom Wai-Hong Tam76c75072011-10-25 18:00:12 +0800120 utils.system('sudo umount %s' % tmp_dir)
121 os.removedirs(tmp_dir)
122 if code != 0:
123 raise error.TestError(
124 'The image in the USB disk should be a test image.')
125
126
Tom Wai-Hong Tama70f0fe2011-09-02 18:28:47 +0800127 def _parse_crossystem_output(self, lines):
128 """Parse the crossystem output into a dict.
129
130 Args:
131 lines: The list of crossystem output strings.
132
133 Returns:
134 A dict which contains the crossystem keys/values.
135
136 Raises:
137 error.TestError: If wrong format in crossystem output.
138
139 >>> seq = FAFTSequence()
140 >>> seq._parse_crossystem_output([ \
141 "arch = x86 # Platform architecture", \
142 "cros_debug = 1 # OS should allow debug", \
143 ])
144 {'cros_debug': '1', 'arch': 'x86'}
145 >>> seq._parse_crossystem_output([ \
146 "arch=x86", \
147 ])
148 Traceback (most recent call last):
149 ...
150 TestError: Failed to parse crossystem output: arch=x86
151 >>> seq._parse_crossystem_output([ \
152 "arch = x86 # Platform architecture", \
153 "arch = arm # Platform architecture", \
154 ])
155 Traceback (most recent call last):
156 ...
157 TestError: Duplicated crossystem key: arch
158 """
159 pattern = "^([^ =]*) *= *(.*[^ ]) *# [^#]*$"
160 parsed_list = {}
161 for line in lines:
162 matched = re.match(pattern, line.strip())
163 if not matched:
164 raise error.TestError("Failed to parse crossystem output: %s"
165 % line)
166 (name, value) = (matched.group(1), matched.group(2))
167 if name in parsed_list:
168 raise error.TestError("Duplicated crossystem key: %s" % name)
169 parsed_list[name] = value
170 return parsed_list
171
172
173 def crossystem_checker(self, expected_dict):
174 """Check the crossystem values matched.
175
176 Given an expect_dict which describes the expected crossystem values,
177 this function check the current crossystem values are matched or not.
178
179 Args:
180 expected_dict: A dict which contains the expected values.
181
182 Returns:
183 True if the crossystem value matched; otherwise, False.
184 """
185 lines = self.faft_client.run_shell_command_get_output('crossystem')
186 got_dict = self._parse_crossystem_output(lines)
187 for key in expected_dict:
188 if key not in got_dict:
189 logging.info('Expected key "%s" not in crossystem result' % key)
190 return False
191 if isinstance(expected_dict[key], str):
192 if got_dict[key] != expected_dict[key]:
193 logging.info("Expected '%s' value '%s' but got '%s'" %
194 (key, expected_dict[key], got_dict[key]))
195 return False
196 elif isinstance(expected_dict[key], tuple):
197 # Expected value is a tuple of possible actual values.
198 if got_dict[key] not in expected_dict[key]:
199 logging.info("Expected '%s' values %s but got '%s'" %
200 (key, str(expected_dict[key]), got_dict[key]))
201 return False
202 else:
203 logging.info("The expected_dict is neither a str nor a dict.")
204 return False
205 return True
206
207
Tom Wai-Hong Tamcfda61f2011-11-02 17:41:01 +0800208 def root_part_checker(self, expected_part):
209 """Check the partition number of the root device matched.
210
211 Args:
212 expected_part: A string containing the number of the expected root
213 partition.
214
215 Returns:
216 True if the currect root partition number matched; otherwise, False.
217 """
Tom Wai-Hong Tam6a863ba2011-12-08 10:13:28 +0800218 part = self.faft_client.get_root_part()[-1]
219 if self.ROOTFS_MAP[expected_part] != part:
220 logging.info("Expected root part %s but got %s" %
221 (self.ROOTFS_MAP[expected_part], part))
222 return False
223 return True
Tom Wai-Hong Tamcfda61f2011-11-02 17:41:01 +0800224
225
Tom Wai-Hong Tamf2103be2011-11-10 07:26:56 +0800226 def _join_part(self, dev, part):
227 """Return a concatenated string of device and partition number.
228
229 Args:
230 dev: A string of device, e.g.'/dev/sda'.
231 part: A string of partition number, e.g.'3'.
232
233 Returns:
234 A concatenated string of device and partition number, e.g.'/dev/sda3'.
235
236 >>> seq = FAFTSequence()
237 >>> seq._join_part('/dev/sda', '3')
238 '/dev/sda3'
239 >>> seq._join_part('/dev/mmcblk0', '2')
240 '/dev/mmcblk0p2'
241 """
242 if 'mmcblk' in dev:
243 return dev + 'p' + part
244 else:
245 return dev + part
246
247
Tom Wai-Hong Tamcfda61f2011-11-02 17:41:01 +0800248 def copy_kernel_and_rootfs(self, from_part, to_part):
249 """Copy kernel and rootfs from from_part to to_part.
250
251 Args:
252 from_part: A string of partition number to be copied from.
Tom Wai-Hong Tamf2103be2011-11-10 07:26:56 +0800253 to_part: A string of partition number to be copied to.
Tom Wai-Hong Tamcfda61f2011-11-02 17:41:01 +0800254 """
255 root_dev = self.faft_client.get_root_dev()
Tom Wai-Hong Tamf2103be2011-11-10 07:26:56 +0800256 logging.info('Copying kernel from %s to %s. Please wait...' %
257 (from_part, to_part))
Tom Wai-Hong Tamcfda61f2011-11-02 17:41:01 +0800258 self.faft_client.run_shell_command('dd if=%s of=%s bs=4M' %
Tom Wai-Hong Tamf2103be2011-11-10 07:26:56 +0800259 (self._join_part(root_dev, self.KERNEL_MAP[from_part]),
260 self._join_part(root_dev, self.KERNEL_MAP[to_part])))
261 logging.info('Copying rootfs from %s to %s. Please wait...' %
262 (from_part, to_part))
Tom Wai-Hong Tamcfda61f2011-11-02 17:41:01 +0800263 self.faft_client.run_shell_command('dd if=%s of=%s bs=4M' %
Tom Wai-Hong Tamf2103be2011-11-10 07:26:56 +0800264 (self._join_part(root_dev, self.ROOTFS_MAP[from_part]),
265 self._join_part(root_dev, self.ROOTFS_MAP[to_part])))
Tom Wai-Hong Tamcfda61f2011-11-02 17:41:01 +0800266
267
268 def ensure_kernel_boot(self, part):
269 """Ensure the request kernel boot.
270
271 If not, it duplicates the current kernel to the requested kernel
272 and sets the requested higher priority to ensure it boot.
273
274 Args:
Tom Wai-Hong Tama9c1a502011-11-10 06:39:26 +0800275 part: A string of kernel partition number or 'a'/'b'.
Tom Wai-Hong Tamcfda61f2011-11-02 17:41:01 +0800276 """
277 if not self.root_part_checker(part):
278 self.copy_kernel_and_rootfs(from_part=self.OTHER_KERNEL_MAP[part],
279 to_part=part)
Tom Wai-Hong Tamc7ecfca2011-12-06 11:12:31 +0800280 self.run_faft_step({
281 'userspace_action': (self.reset_and_prioritize_kernel, part),
282 })
Tom Wai-Hong Tamcfda61f2011-11-02 17:41:01 +0800283
284
Tom Wai-Hong Tama9c1a502011-11-10 06:39:26 +0800285 def wait_fw_screen_and_ctrl_d(self):
286 """Wait for firmware warning screen and press Ctrl-D."""
287 time.sleep(self.FIRMWARE_SCREEN_DELAY)
288 self.servo.ctrl_d()
289
290
Tom Wai-Hong Tam5d2f4702011-12-06 10:42:31 +0800291 def wait_fw_screen_and_plug_usb(self):
292 """Wait for firmware warning screen and then unplug and plug the USB."""
293 time.sleep(self.FIRMWARE_SCREEN_DELAY)
294 self.servo.set('usb_mux_sel1', 'servo_sees_usbkey')
295 time.sleep(self.USB_PLUG_DELAY)
296 self.servo.set('usb_mux_sel1', 'dut_sees_usbkey')
297
298
Tom Wai-Hong Tamfd590c92011-11-25 11:50:57 +0800299 def setup_tried_fwb(self, tried_fwb):
300 """Setup for fw B tried state.
301
302 It makes sure the system in the requested fw B tried state. If not, it
303 tries to do so.
304
305 Args:
306 tried_fwb: True if requested in tried_fwb=1; False if tried_fwb=0.
307 """
308 if tried_fwb:
309 if not self.crossystem_checker({'tried_fwb': '1'}):
310 logging.info(
311 'Firmware is not booted with tried_fwb. Reboot into it.')
312 self.run_faft_step({
313 'userspace_action': self.faft_client.set_try_fw_b,
314 })
315 else:
316 if not self.crossystem_checker({'tried_fwb': '0'}):
317 logging.info(
318 'Firmware is booted with tried_fwb. Reboot to clear.')
319 self.run_faft_step({})
320
321
Tom Wai-Hong Tama9c1a502011-11-10 06:39:26 +0800322 def setup_dev_mode(self, dev_mode):
323 """Setup for development mode.
324
Tom Wai-Hong Tamfd590c92011-11-25 11:50:57 +0800325 It makes sure the system in the requested normal/dev mode. If not, it
Tom Wai-Hong Tama9c1a502011-11-10 06:39:26 +0800326 tries to do so.
327
328 Args:
329 dev_mode: True if requested in dev mode; False if normal mode.
330 """
Tom Wai-Hong Tamfd590c92011-11-25 11:50:57 +0800331 # Change the default firmware_action for dev mode passing the fw screen.
332 self.register_faft_template({
333 'state_checker': (None),
334 'userspace_action': (None),
335 'reboot_action': (self.sync_and_hw_reboot),
336 'firmware_action': (self.wait_fw_screen_and_ctrl_d if dev_mode
337 else None),
338 })
Tom Wai-Hong Tama9c1a502011-11-10 06:39:26 +0800339 if dev_mode:
340 if not self.crossystem_checker({'devsw_cur': '1'}):
341 logging.info('Dev switch is not on. Now switch it on.')
342 self.servo.enable_development_mode()
343 if not self.crossystem_checker({'devsw_boot': '1',
344 'mainfw_type': 'developer'}):
345 logging.info('System is not in dev mode. Reboot into it.')
Tom Wai-Hong Tamfd590c92011-11-25 11:50:57 +0800346 self.run_faft_step({
347 'userspace_action': (self.faft_client.run_shell_command,
Tom Wai-Hong Tamc7ecfca2011-12-06 11:12:31 +0800348 'chromeos-firmwareupdate --mode todev && reboot'),
349 'reboot_action': None,
Tom Wai-Hong Tamfd590c92011-11-25 11:50:57 +0800350 })
Tom Wai-Hong Tama9c1a502011-11-10 06:39:26 +0800351 else:
352 if not self.crossystem_checker({'devsw_cur': '0'}):
353 logging.info('Dev switch is not off. Now switch it off.')
354 self.servo.disable_development_mode()
355 if not self.crossystem_checker({'devsw_boot': '0',
356 'mainfw_type': 'normal'}):
357 logging.info('System is not in normal mode. Reboot into it.')
Tom Wai-Hong Tamfd590c92011-11-25 11:50:57 +0800358 self.run_faft_step({
359 'userspace_action': (self.faft_client.run_shell_command,
Tom Wai-Hong Tamc7ecfca2011-12-06 11:12:31 +0800360 'chromeos-firmwareupdate --mode tonormal && reboot'),
361 'reboot_action': None,
Tom Wai-Hong Tamfd590c92011-11-25 11:50:57 +0800362 })
Tom Wai-Hong Tama9c1a502011-11-10 06:39:26 +0800363
364
Tom Wai-Hong Tamcfda61f2011-11-02 17:41:01 +0800365 def setup_kernel(self, part):
366 """Setup for kernel test.
367
368 It makes sure both kernel A and B bootable and the current boot is
369 the requested kernel part.
370
371 Args:
Tom Wai-Hong Tama9c1a502011-11-10 06:39:26 +0800372 part: A string of kernel partition number or 'a'/'b'.
Tom Wai-Hong Tamcfda61f2011-11-02 17:41:01 +0800373 """
374 self.ensure_kernel_boot(part)
375 self.copy_kernel_and_rootfs(from_part=part,
376 to_part=self.OTHER_KERNEL_MAP[part])
377 self.reset_and_prioritize_kernel(part)
378
379
380 def reset_and_prioritize_kernel(self, part):
381 """Make the requested partition highest priority.
382
383 This function also reset kerenl A and B to bootable.
384
385 Args:
Tom Wai-Hong Tama9c1a502011-11-10 06:39:26 +0800386 part: A string of partition number to be prioritized.
Tom Wai-Hong Tamcfda61f2011-11-02 17:41:01 +0800387 """
388 root_dev = self.faft_client.get_root_dev()
389 # Reset kernel A and B to bootable.
390 self.faft_client.run_shell_command('cgpt add -i%s -P1 -S1 -T0 %s' %
391 (self.KERNEL_MAP['a'], root_dev))
392 self.faft_client.run_shell_command('cgpt add -i%s -P1 -S1 -T0 %s' %
393 (self.KERNEL_MAP['b'], root_dev))
394 # Set kernel part highest priority.
395 self.faft_client.run_shell_command('cgpt prioritize -i%s %s' %
396 (self.KERNEL_MAP[part], root_dev))
Tom Wai-Hong Tam6a863ba2011-12-08 10:13:28 +0800397 # Safer to sync and wait until the cgpt status written to the disk.
398 self.faft_client.run_shell_command('sync')
399 time.sleep(self.SYNC_DELAY)
Tom Wai-Hong Tamcfda61f2011-11-02 17:41:01 +0800400
401
Tom Wai-Hong Tamf1e34972011-11-02 17:07:04 +0800402 def sync_and_hw_reboot(self):
403 """Request the client sync and do a warm reboot.
404
405 This is the default reboot action on FAFT.
406 """
407 self.faft_client.run_shell_command('sync')
Tom Wai-Hong Tam6a863ba2011-12-08 10:13:28 +0800408 time.sleep(self.SYNC_DELAY)
Tom Wai-Hong Tamf1e34972011-11-02 17:07:04 +0800409 self.servo.warm_reset()
410
411
Tom Wai-Hong Tama70f0fe2011-09-02 18:28:47 +0800412 def _str_action(self, action):
413 """Convert the action function into a readable string.
414
415 The simple str() doesn't work on remote objects since we disable
416 allow_dotted_names flag when we launch the SimpleXMLRPCServer.
417 So this function handles the exception in this case.
418
419 Args:
420 action: A function.
421
422 Returns:
423 A readable string.
424 """
425 try:
426 return str(action)
427 except xmlrpclib.Fault:
428 return '<remote method>'
429
430
431 def _call_action(self, action_tuple):
432 """Call the action function with/without arguments.
433
434 Args:
435 action_tuple: A function, or a tuple which consisted of a function
436 and its arguments (if any).
437
438 Returns:
439 The result value of the action function.
440 """
441 if isinstance(action_tuple, tuple):
442 action = action_tuple[0]
443 args = action_tuple[1:]
444 if callable(action):
445 logging.info('calling %s with parameter %s' % (
446 self._str_action(action), str(action_tuple[1])))
447 return action(*args)
448 else:
449 logging.info('action is not callable!')
450 else:
451 action = action_tuple
452 if action is not None:
453 if callable(action):
454 logging.info('calling %s' % self._str_action(action))
455 return action()
456 else:
457 logging.info('action is not callable!')
458
459 return None
460
461
462 def register_faft_template(self, template):
463 """Register FAFT template, the default FAFT_STEP of each step.
464
465 Args:
466 template: A FAFT_STEP dict.
467 """
468 self._faft_template = template
469
470
471 def register_faft_sequence(self, sequence):
472 """Register FAFT sequence.
473
474 Args:
475 sequence: A FAFT_SEQUENCE array which consisted of FAFT_STEP dicts.
476 """
477 self._faft_sequence = sequence
478
479
Tom Wai-Hong Tam2c50dff2011-11-11 07:01:01 +0800480 def run_faft_step(self, step, no_reboot=False):
481 """Run a single FAFT step.
482
483 Any missing field falls back to faft_template. An empty step means
484 running the default faft_template.
485
486 Args:
487 step: A FAFT_STEP dict.
488 no_reboot: True to prevent running reboot_action and firmware_action.
Tom Wai-Hong Tama70f0fe2011-09-02 18:28:47 +0800489
490 Raises:
Tom Wai-Hong Tama9c1a502011-11-10 06:39:26 +0800491 error.TestFail: An error when the test failed.
Tom Wai-Hong Tama70f0fe2011-09-02 18:28:47 +0800492 """
Tom Wai-Hong Tam2c50dff2011-11-11 07:01:01 +0800493 test = {}
494 test.update(self._faft_template)
495 test.update(step)
496
497 if test['state_checker']:
498 if not self._call_action(test['state_checker']):
499 raise error.TestFail('State checker failed!')
500
501 self._call_action(test['userspace_action'])
502
503 # Don't run reboot_action and firmware_action if no_reboot is True.
504 if not no_reboot:
505 self._call_action(test['reboot_action'])
506 self.wait_for_client_offline()
507 self._call_action(test['firmware_action'])
508
509 if 'install_deps_after_boot' in test:
510 self.wait_for_client(
511 install_deps=test['install_deps_after_boot'])
512 else:
513 self.wait_for_client()
514
515
516 def run_faft_sequence(self):
517 """Run FAFT sequence which was previously registered."""
Tom Wai-Hong Tama70f0fe2011-09-02 18:28:47 +0800518 sequence = self._faft_sequence
Tom Wai-Hong Tam2c50dff2011-11-11 07:01:01 +0800519 for step in sequence:
520 # Don't reboot in the last step.
521 self.run_faft_step(step, no_reboot=(step is sequence[-1]))