Change the whitelist to a blacklist.

Various fixes, including 4-to-2-space-indentation change.

BUG=None
TEST=make lint, make test

Change-Id: Ic41e96936b47e66833a9773d36779c004f69aae1
Reviewed-on: https://gerrit.chromium.org/gerrit/26773
Commit-Ready: Jon Salz <jsalz@chromium.org>
Reviewed-by: Jon Salz <jsalz@chromium.org>
Tested-by: Jon Salz <jsalz@chromium.org>
diff --git a/py/goofy/event_log_watcher_unittest.py b/py/goofy/event_log_watcher_unittest.py
index 1dbe399..ef51c3d 100755
--- a/py/goofy/event_log_watcher_unittest.py
+++ b/py/goofy/event_log_watcher_unittest.py
@@ -4,7 +4,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import factory_common
+import factory_common  # pylint: disable=W0611
 
 import logging
 import mox
diff --git a/py/goofy/goofy.py b/py/goofy/goofy.py
index b69f1b6..35644e8 100755
--- a/py/goofy/goofy.py
+++ b/py/goofy/goofy.py
@@ -10,29 +10,19 @@
 The main factory flow that runs the factory test and finalizes a device.
 '''
 
-import array
-import fcntl
-import glob
 import logging
 import os
-import cPickle as pickle
-import pipes
 import Queue
-import re
-import signal
 import subprocess
 import sys
-import tempfile
 import threading
 import time
 import traceback
-import unittest
 import uuid
 from collections import deque
 from optparse import OptionParser
-from StringIO import StringIO
 
-import factory_common
+import factory_common  # pylint: disable=W0611
 from cros.factory.goofy.prespawner import Prespawner
 from cros.factory.test import factory
 from cros.factory.test import state
@@ -64,1010 +54,1013 @@
 
 To use Goofy in the chroot, first install an Xvnc server:
 
-    sudo apt-get install tightvncserver
+  sudo apt-get install tightvncserver
 
 ...and then start a VNC X server outside the chroot:
 
-    vncserver :10 &
-    vncviewer :10
+  vncserver :10 &
+  vncviewer :10
 
 ...and run Goofy as follows:
 
-    env --unset=XAUTHORITY DISPLAY=localhost:10 python goofy.py
+  env --unset=XAUTHORITY DISPLAY=localhost:10 python goofy.py
 ''' + ('*' * 70)
 suppress_chroot_warning = False
 
 def get_hwid_cfg():
-    '''
-    Returns the HWID config tag, or an empty string if none can be found.
-    '''
-    if 'CROS_HWID' in os.environ:
-        return os.environ['CROS_HWID']
-    if os.path.exists(HWID_CFG_PATH):
-        with open(HWID_CFG_PATH, 'rt') as hwid_cfg_handle:
-            return hwid_cfg_handle.read().strip()
-    return ''
+  '''
+  Returns the HWID config tag, or an empty string if none can be found.
+  '''
+  if 'CROS_HWID' in os.environ:
+    return os.environ['CROS_HWID']
+  if os.path.exists(HWID_CFG_PATH):
+    with open(HWID_CFG_PATH, 'rt') as hwid_cfg_handle:
+      return hwid_cfg_handle.read().strip()
+  return ''
 
 
 def find_test_list():
-    '''
-    Returns the path to the active test list, based on the HWID config tag.
-    '''
-    hwid_cfg = get_hwid_cfg()
+  '''
+  Returns the path to the active test list, based on the HWID config tag.
+  '''
+  hwid_cfg = get_hwid_cfg()
 
-    search_dirs = [CUSTOM_DIR, DEFAULT_TEST_LISTS_DIR]
+  search_dirs = [CUSTOM_DIR, DEFAULT_TEST_LISTS_DIR]
 
-    # Try in order: test_list_${hwid_cfg}, test_list, test_list.all
-    search_files = ['test_list', 'test_list.all']
-    if hwid_cfg:
-        search_files.insert(0, hwid_cfg)
+  # Try in order: test_list_${hwid_cfg}, test_list, test_list.all
+  search_files = ['test_list', 'test_list.all']
+  if hwid_cfg:
+    search_files.insert(0, hwid_cfg)
 
-    for d in search_dirs:
-        for f in search_files:
-            test_list = os.path.join(d, f)
-            if os.path.exists(test_list):
-                return test_list
+  for d in search_dirs:
+    for f in search_files:
+      test_list = os.path.join(d, f)
+      if os.path.exists(test_list):
+        return test_list
 
-    logging.warn('Cannot find test lists named any of %s in any of %s',
-                 search_files, search_dirs)
-    return None
+  logging.warn('Cannot find test lists named any of %s in any of %s',
+         search_files, search_dirs)
+  return None
 
 _inited_logging = False
 
 class Goofy(object):
+  '''
+  The main factory flow.
+
+  Note that all methods in this class must be invoked from the main
+  (event) thread.  Other threads, such as callbacks and TestInvocation
+  methods, should instead post events on the run queue.
+
+  TODO: Unit tests. (chrome-os-partner:7409)
+
+  Properties:
+    uuid: A unique UUID for this invocation of Goofy.
+    state_instance: An instance of FactoryState.
+    state_server: The FactoryState XML/RPC server.
+    state_server_thread: A thread running state_server.
+    event_server: The EventServer socket server.
+    event_server_thread: A thread running event_server.
+    event_client: A client to the event server.
+    connection_manager: The connection_manager object.
+    network_enabled: Whether the connection_manager is currently
+      enabling connections.
+    ui_process: The factory ui process object.
+    run_queue: A queue of callbacks to invoke from the main thread.
+    invocations: A map from FactoryTest objects to the corresponding
+      TestInvocations objects representing active tests.
+    tests_to_run: A deque of tests that should be run when the current
+      test(s) complete.
+    options: Command-line options.
+    args: Command-line args.
+    test_list: The test list.
+    event_handlers: Map of Event.Type to the method used to handle that
+      event.  If the method has an 'event' argument, the event is passed
+      to the handler.
+    exceptions: Exceptions encountered in invocation threads.
+  '''
+  def __init__(self):
+    self.uuid = str(uuid.uuid4())
+    self.state_instance = None
+    self.state_server = None
+    self.state_server_thread = None
+    self.event_server = None
+    self.event_server_thread = None
+    self.event_client = None
+    self.connection_manager = None
+    self.log_watcher = None
+    self.network_enabled = True
+    self.event_log = None
+    self.prespawner = None
+    self.ui_process = None
+    self.run_queue = Queue.Queue()
+    self.invocations = {}
+    self.tests_to_run = deque()
+    self.visible_test = None
+    self.chrome = None
+
+    self.options = None
+    self.args = None
+    self.test_list = None
+    self.on_ui_startup = []
+    self.env = None
+    self.last_shutdown_time = None
+
+    def test_or_root(event):
+      '''Returns the top-level parent for a test (the root node of the
+      tests that need to be run together if the given test path is to
+      be run).'''
+      try:
+        path = event.path
+      except AttributeError:
+        path = None
+
+      if path:
+        return (self.test_list.lookup_path(path).
+            get_top_level_parent_or_group())
+      else:
+        return self.test_list
+
+    self.event_handlers = {
+      Event.Type.SWITCH_TEST: self.handle_switch_test,
+      Event.Type.SHOW_NEXT_ACTIVE_TEST:
+        lambda event: self.show_next_active_test(),
+      Event.Type.RESTART_TESTS:
+        lambda event: self.restart_tests(root=test_or_root(event)),
+      Event.Type.AUTO_RUN:
+        lambda event: self.auto_run(root=test_or_root(event)),
+      Event.Type.RE_RUN_FAILED:
+        lambda event: self.re_run_failed(root=test_or_root(event)),
+      Event.Type.RUN_TESTS_WITH_STATUS:
+        lambda event: self.run_tests_with_status(
+          event.status,
+          root=test_or_root(event)),
+      Event.Type.REVIEW:
+        lambda event: self.show_review_information(),
+      Event.Type.UPDATE_SYSTEM_INFO:
+        lambda event: self.update_system_info(),
+      Event.Type.UPDATE_FACTORY:
+        lambda event: self.update_factory(),
+      Event.Type.STOP:
+        lambda event: self.stop(),
+    }
+
+    self.exceptions = []
+    self.web_socket_manager = None
+
+  def destroy(self):
+    if self.chrome:
+      self.chrome.kill()
+      self.chrome = None
+    if self.ui_process:
+      utils.kill_process_tree(self.ui_process, 'ui')
+      self.ui_process = None
+    if self.web_socket_manager:
+      logging.info('Stopping web sockets')
+      self.web_socket_manager.close()
+      self.web_socket_manager = None
+    if self.state_server_thread:
+      logging.info('Stopping state server')
+      self.state_server.shutdown()
+      self.state_server_thread.join()
+      self.state_server.server_close()
+      self.state_server_thread = None
+    if self.state_instance:
+      self.state_instance.close()
+    if self.event_server_thread:
+      logging.info('Stopping event server')
+      self.event_server.shutdown()  # pylint: disable=E1101
+      self.event_server_thread.join()
+      self.event_server.server_close()
+      self.event_server_thread = None
+    if self.log_watcher:
+      if self.log_watcher.IsThreadStarted():
+        self.log_watcher.StopWatchThread()
+      self.log_watcher = None
+    if self.prespawner:
+      logging.info('Stopping prespawner')
+      self.prespawner.stop()
+      self.prespawner = None
+    if self.event_client:
+      logging.info('Closing event client')
+      self.event_client.close()
+      self.event_client = None
+    if self.event_log:
+      self.event_log.Close()
+      self.event_log = None
+    self.check_exceptions()
+    logging.info('Done destroying Goofy')
+
+  def start_state_server(self):
+    self.state_instance, self.state_server = (
+      state.create_server(bind_address='0.0.0.0'))
+    logging.info('Starting state server')
+    self.state_server_thread = threading.Thread(
+      target=self.state_server.serve_forever,
+      name='StateServer')
+    self.state_server_thread.start()
+
+  def start_event_server(self):
+    self.event_server = EventServer()
+    logging.info('Starting factory event server')
+    self.event_server_thread = threading.Thread(
+      target=self.event_server.serve_forever,
+      name='EventServer')  # pylint: disable=E1101
+    self.event_server_thread.start()
+
+    self.event_client = EventClient(
+      callback=self.handle_event, event_loop=self.run_queue)
+
+    self.web_socket_manager = WebSocketManager(self.uuid)
+    self.state_server.add_handler("/event",
+      self.web_socket_manager.handle_web_socket)
+
+  def start_ui(self):
+    ui_proc_args = [
+      os.path.join(factory.FACTORY_PACKAGE_PATH, 'test', 'ui.py'),
+      self.options.test_list]
+    if self.options.verbose:
+      ui_proc_args.append('-v')
+    logging.info('Starting ui %s', ui_proc_args)
+    self.ui_process = subprocess.Popen(ui_proc_args)
+    logging.info('Waiting for UI to come up...')
+    self.event_client.wait(
+      lambda event: event.type == Event.Type.UI_READY)
+    logging.info('UI has started')
+
+  def set_visible_test(self, test):
+    if self.visible_test == test:
+      return
+
+    if test:
+      test.update_state(visible=True)
+    if self.visible_test:
+      self.visible_test.update_state(visible=False)
+    self.visible_test = test
+
+  def handle_shutdown_complete(self, test, test_state):
     '''
-    The main factory flow.
+    Handles the case where a shutdown was detected during a shutdown step.
 
-    Note that all methods in this class must be invoked from the main
-    (event) thread.  Other threads, such as callbacks and TestInvocation
-    methods, should instead post events on the run queue.
-
-    TODO: Unit tests. (chrome-os-partner:7409)
-
-    Properties:
-        uuid: A unique UUID for this invocation of Goofy.
-        state_instance: An instance of FactoryState.
-        state_server: The FactoryState XML/RPC server.
-        state_server_thread: A thread running state_server.
-        event_server: The EventServer socket server.
-        event_server_thread: A thread running event_server.
-        event_client: A client to the event server.
-        connection_manager: The connection_manager object.
-        network_enabled: Whether the connection_manager is currently
-            enabling connections.
-        ui_process: The factory ui process object.
-        run_queue: A queue of callbacks to invoke from the main thread.
-        invocations: A map from FactoryTest objects to the corresponding
-            TestInvocations objects representing active tests.
-        tests_to_run: A deque of tests that should be run when the current
-            test(s) complete.
-        options: Command-line options.
-        args: Command-line args.
-        test_list: The test list.
-        event_handlers: Map of Event.Type to the method used to handle that
-            event.  If the method has an 'event' argument, the event is passed
-            to the handler.
-        exceptions: Exceptions encountered in invocation threads.
+    @param test: The ShutdownStep.
+    @param test_state: The test state.
     '''
-    def __init__(self):
-        self.uuid = str(uuid.uuid4())
-        self.state_instance = None
-        self.state_server = None
-        self.state_server_thread = None
-        self.event_server = None
-        self.event_server_thread = None
-        self.event_client = None
-        self.connection_manager = None
-        self.log_watcher = None
-        self.network_enabled = True
-        self.event_log = None
-        self.prespawner = None
-        self.ui_process = None
-        self.run_queue = Queue.Queue()
-        self.invocations = {}
-        self.tests_to_run = deque()
-        self.visible_test = None
-        self.chrome = None
+    test_state = test.update_state(increment_shutdown_count=1)
+    logging.info('Detected shutdown (%d of %d)',
+           test_state.shutdown_count, test.iterations)
 
-        self.options = None
-        self.args = None
-        self.test_list = None
-        self.on_ui_startup = []
+    def log_and_update_state(status, error_msg, **kw):
+      self.event_log.Log('rebooted',
+                 status=status, error_msg=error_msg, **kw)
+      test.update_state(status=status, error_msg=error_msg)
 
-        def test_or_root(event):
-            '''Returns the top-level parent for a test (the root node of the
-            tests that need to be run together if the given test path is to
-            be run).'''
-            try:
-                path = event.path
-            except AttributeError:
-                path = None
+    if not self.last_shutdown_time:
+      log_and_update_state(status=TestState.FAILED,
+                 error_msg='Unable to read shutdown_time')
+      return
 
-            if path:
-                return (self.test_list.lookup_path(path).
-                        get_top_level_parent_or_group())
-            else:
-                return self.test_list
+    now = time.time()
+    logging.info('%.03f s passed since reboot',
+           now - self.last_shutdown_time)
 
-        self.event_handlers = {
-            Event.Type.SWITCH_TEST: self.handle_switch_test,
-            Event.Type.SHOW_NEXT_ACTIVE_TEST:
-                lambda event: self.show_next_active_test(),
-            Event.Type.RESTART_TESTS:
-                lambda event: self.restart_tests(root=test_or_root(event)),
-            Event.Type.AUTO_RUN:
-                lambda event: self.auto_run(root=test_or_root(event)),
-            Event.Type.RE_RUN_FAILED:
-                lambda event: self.re_run_failed(root=test_or_root(event)),
-            Event.Type.RUN_TESTS_WITH_STATUS:
-                lambda event: self.run_tests_with_status(
-                    event.status,
-                    root=test_or_root(event)),
-            Event.Type.REVIEW:
-                lambda event: self.show_review_information(),
-            Event.Type.UPDATE_SYSTEM_INFO:
-                lambda event: self.update_system_info(),
-            Event.Type.UPDATE_FACTORY:
-                lambda event: self.update_factory(),
-            Event.Type.STOP:
-                lambda event: self.stop(),
-        }
+    if self.last_shutdown_time > now:
+      test.update_state(status=TestState.FAILED,
+                error_msg='Time moved backward during reboot')
+    elif (isinstance(test, factory.RebootStep) and
+        self.test_list.options.max_reboot_time_secs and
+        (now - self.last_shutdown_time >
+         self.test_list.options.max_reboot_time_secs)):
+      # A reboot took too long; fail.  (We don't check this for
+      # HaltSteps, because the machine could be halted for a
+      # very long time, and even unplugged with battery backup,
+      # thus hosing the clock.)
+      log_and_update_state(
+        status=TestState.FAILED,
+        error_msg=('More than %d s elapsed during reboot '
+               '(%.03f s, from %s to %s)' % (
+            self.test_list.options.max_reboot_time_secs,
+            now - self.last_shutdown_time,
+            utils.TimeString(self.last_shutdown_time),
+            utils.TimeString(now))),
+        duration=(now-self.last_shutdown_time))
+    elif test_state.shutdown_count == test.iterations:
+      # Good!
+      log_and_update_state(status=TestState.PASSED,
+                 duration=(now - self.last_shutdown_time),
+                 error_msg='')
+    elif test_state.shutdown_count > test.iterations:
+      # Shut down too many times
+      log_and_update_state(status=TestState.FAILED,
+                 error_msg='Too many shutdowns')
+    elif utils.are_shift_keys_depressed():
+      logging.info('Shift keys are depressed; cancelling restarts')
+      # Abort shutdown
+      log_and_update_state(
+        status=TestState.FAILED,
+        error_msg='Shutdown aborted with double shift keys')
+    else:
+      def handler():
+        if self._prompt_cancel_shutdown(
+          test, test_state.shutdown_count + 1):
+          log_and_update_state(
+            status=TestState.FAILED,
+            error_msg='Shutdown aborted by operator')
+          return
 
-        self.exceptions = []
-        self.web_socket_manager = None
+        # Time to shutdown again
+        log_and_update_state(
+          status=TestState.ACTIVE,
+          error_msg='',
+          iteration=test_state.shutdown_count)
 
-    def destroy(self):
-        if self.chrome:
-            self.chrome.kill()
-            self.chrome = None
-        if self.ui_process:
-            utils.kill_process_tree(self.ui_process, 'ui')
-            self.ui_process = None
-        if self.web_socket_manager:
-            logging.info('Stopping web sockets')
-            self.web_socket_manager.close()
-            self.web_socket_manager = None
-        if self.state_server_thread:
-            logging.info('Stopping state server')
-            self.state_server.shutdown()
-            self.state_server_thread.join()
-            self.state_server.server_close()
-            self.state_server_thread = None
-        if self.state_instance:
-            self.state_instance.close()
-        if self.event_server_thread:
-            logging.info('Stopping event server')
-            self.event_server.shutdown()  # pylint: disable=E1101
-            self.event_server_thread.join()
-            self.event_server.server_close()
-            self.event_server_thread = None
-        if self.log_watcher:
-            if self.log_watcher.IsThreadStarted():
-                self.log_watcher.StopWatchThread()
-            self.log_watcher = None
-        if self.prespawner:
-            logging.info('Stopping prespawner')
-            self.prespawner.stop()
-            self.prespawner = None
-        if self.event_client:
-            logging.info('Closing event client')
-            self.event_client.close()
-            self.event_client = None
-        if self.event_log:
-            self.event_log.Close()
-            self.event_log = None
-        self.check_exceptions()
-        logging.info('Done destroying Goofy')
+        self.event_log.Log('shutdown', operation='reboot')
+        self.state_instance.set_shared_data('shutdown_time',
+                        time.time())
+        self.env.shutdown('reboot')
 
-    def start_state_server(self):
-        self.state_instance, self.state_server = (
-            state.create_server(bind_address='0.0.0.0'))
-        logging.info('Starting state server')
-        self.state_server_thread = threading.Thread(
-            target=self.state_server.serve_forever,
-            name='StateServer')
-        self.state_server_thread.start()
+      self.on_ui_startup.append(handler)
 
-    def start_event_server(self):
-        self.event_server = EventServer()
-        logging.info('Starting factory event server')
-        self.event_server_thread = threading.Thread(
-            target=self.event_server.serve_forever,
-            name='EventServer')  # pylint: disable=E1101
-        self.event_server_thread.start()
+  def _prompt_cancel_shutdown(self, test, iteration):
+    if self.options.ui != 'chrome':
+      return False
 
-        self.event_client = EventClient(
-            callback=self.handle_event, event_loop=self.run_queue)
+    pending_shutdown_data = {
+      'delay_secs': test.delay_secs,
+      'time': time.time() + test.delay_secs,
+      'operation': test.operation,
+      'iteration': iteration,
+      'iterations': test.iterations,
+      }
 
-        self.web_socket_manager = WebSocketManager(self.uuid)
-        self.state_server.add_handler("/event",
-            self.web_socket_manager.handle_web_socket)
+    # Create a new (threaded) event client since we
+    # don't want to use the event loop for this.
+    with EventClient() as event_client:
+      event_client.post_event(Event(Event.Type.PENDING_SHUTDOWN,
+                      **pending_shutdown_data))
+      aborted = event_client.wait(
+        lambda event: event.type == Event.Type.CANCEL_SHUTDOWN,
+        timeout=test.delay_secs) is not None
+      if aborted:
+        event_client.post_event(Event(Event.Type.PENDING_SHUTDOWN))
+      return aborted
 
-    def start_ui(self):
-        ui_proc_args = [
-            os.path.join(factory.FACTORY_PACKAGE_PATH, 'test', 'ui.py'),
-            self.options.test_list]
-        if self.options.verbose:
-            ui_proc_args.append('-v')
-        logging.info('Starting ui %s', ui_proc_args)
-        self.ui_process = subprocess.Popen(ui_proc_args)
-        logging.info('Waiting for UI to come up...')
-        self.event_client.wait(
-            lambda event: event.type == Event.Type.UI_READY)
-        logging.info('UI has started')
+  def init_states(self):
+    '''
+    Initializes all states on startup.
+    '''
+    for test in self.test_list.get_all_tests():
+      # Make sure the state server knows about all the tests,
+      # defaulting to an untested state.
+      test.update_state(update_parent=False, visible=False)
 
-    def set_visible_test(self, test):
-        if self.visible_test == test:
-            return
+    var_log_messages = None
 
-        if test:
-            test.update_state(visible=True)
-        if self.visible_test:
-            self.visible_test.update_state(visible=False)
-        self.visible_test = test
+    # Any 'active' tests should be marked as failed now.
+    for test in self.test_list.walk():
+      test_state = test.get_state()
+      if test_state.status != TestState.ACTIVE:
+        continue
+      if isinstance(test, factory.ShutdownStep):
+        # Shutdown while the test was active - that's good.
+        self.handle_shutdown_complete(test, test_state)
+      else:
+        # Unexpected shutdown.  Grab /var/log/messages for context.
+        if var_log_messages is None:
+          try:
+            var_log_messages = (
+              utils.var_log_messages_before_reboot())
+            # Write it to the log, to make it easier to
+            # correlate with /var/log/messages.
+            logging.info(
+              'Unexpected shutdown. '
+              'Tail of /var/log/messages before last reboot:\n'
+              '%s', ('\n'.join(
+                  '  ' + x for x in var_log_messages)))
+          except:  # pylint: disable=W0702
+            logging.exception('Unable to grok /var/log/messages')
+            var_log_messages = []
 
-    def handle_shutdown_complete(self, test, state):
-        '''
-        Handles the case where a shutdown was detected during a shutdown step.
+        error_msg = 'Unexpected shutdown while test was running'
+        self.event_log.Log('end_test',
+                   path=test.path,
+                   status=TestState.FAILED,
+                   invocation=test.get_state().invocation,
+                   error_msg=error_msg,
+                   var_log_messages='\n'.join(var_log_messages))
+        test.update_state(
+          status=TestState.FAILED,
+          error_msg=error_msg)
 
-        @param test: The ShutdownStep.
-        @param state: The test state.
-        '''
-        state = test.update_state(increment_shutdown_count=1)
-        logging.info('Detected shutdown (%d of %d)',
-                     state.shutdown_count, test.iterations)
+  def show_next_active_test(self):
+    '''
+    Rotates to the next visible active test.
+    '''
+    self.reap_completed_tests()
+    active_tests = [
+      t for t in self.test_list.walk()
+      if t.is_leaf() and t.get_state().status == TestState.ACTIVE]
+    if not active_tests:
+      return
 
-        def log_and_update_state(status, error_msg, **kw):
-            self.event_log.Log('rebooted',
-                               status=status, error_msg=error_msg, **kw)
-            test.update_state(status=status, error_msg=error_msg)
+    try:
+      next_test = active_tests[
+        (active_tests.index(self.visible_test) + 1) % len(active_tests)]
+    except ValueError:  # visible_test not present in active_tests
+      next_test = active_tests[0]
 
-        if not self.last_shutdown_time:
-            log_and_update_state(status=TestState.FAILED,
-                                 error_msg='Unable to read shutdown_time')
-            return
+    self.set_visible_test(next_test)
 
-        now = time.time()
-        logging.info('%.03f s passed since reboot',
-                     now - self.last_shutdown_time)
+  def handle_event(self, event):
+    '''
+    Handles an event from the event server.
+    '''
+    handler = self.event_handlers.get(event.type)
+    if handler:
+      handler(event)
+    else:
+      # We don't register handlers for all event types - just ignore
+      # this event.
+      logging.debug('Unbound event type %s', event.type)
 
-        if self.last_shutdown_time > now:
-            test.update_state(status=TestState.FAILED,
-                              error_msg='Time moved backward during reboot')
-        elif (isinstance(test, factory.RebootStep) and
-              self.test_list.options.max_reboot_time_secs and
-              (now - self.last_shutdown_time >
-               self.test_list.options.max_reboot_time_secs)):
-            # A reboot took too long; fail.  (We don't check this for
-            # HaltSteps, because the machine could be halted for a
-            # very long time, and even unplugged with battery backup,
-            # thus hosing the clock.)
-            log_and_update_state(
-                status=TestState.FAILED,
-                error_msg=('More than %d s elapsed during reboot '
-                           '(%.03f s, from %s to %s)' % (
-                        self.test_list.options.max_reboot_time_secs,
-                        now - self.last_shutdown_time,
-                        utils.TimeString(self.last_shutdown_time),
-                        utils.TimeString(now))),
-                duration=(now-self.last_shutdown_time))
-        elif state.shutdown_count == test.iterations:
-            # Good!
-            log_and_update_state(status=TestState.PASSED,
-                                 duration=(now - self.last_shutdown_time),
-                                 error_msg='')
-        elif state.shutdown_count > test.iterations:
-            # Shut down too many times
-            log_and_update_state(status=TestState.FAILED,
-                                 error_msg='Too many shutdowns')
-        elif utils.are_shift_keys_depressed():
-            logging.info('Shift keys are depressed; cancelling restarts')
-            # Abort shutdown
-            log_and_update_state(
-                status=TestState.FAILED,
-                error_msg='Shutdown aborted with double shift keys')
-        else:
-            def handler():
-                if self._prompt_cancel_shutdown(test, state.shutdown_count + 1):
-                    log_and_update_state(
-                        status=TestState.FAILED,
-                        error_msg='Shutdown aborted by operator')
-                    return
+  def run_next_test(self):
+    '''
+    Runs the next eligible test (or tests) in self.tests_to_run.
+    '''
+    self.reap_completed_tests()
+    while self.tests_to_run:
+      logging.debug('Tests to run: %s',
+              [x.path for x in self.tests_to_run])
 
-                # Time to shutdown again
-                log_and_update_state(
-                    status=TestState.ACTIVE,
-                    error_msg='',
-                    iteration=state.shutdown_count)
+      test = self.tests_to_run[0]
 
-                self.event_log.Log('shutdown', operation='reboot')
-                self.state_instance.set_shared_data('shutdown_time',
-                                                time.time())
-                self.env.shutdown('reboot')
+      if test in self.invocations:
+        logging.info('Next test %s is already running', test.path)
+        self.tests_to_run.popleft()
+        return
 
-            self.on_ui_startup.append(handler)
+      if self.invocations and not (test.backgroundable and all(
+        [x.backgroundable for x in self.invocations])):
+        logging.debug('Waiting for non-backgroundable tests to '
+                'complete before running %s', test.path)
+        return
 
-    def _prompt_cancel_shutdown(self, test, iteration):
-        if self.options.ui != 'chrome':
-            return False
+      self.tests_to_run.popleft()
 
-        pending_shutdown_data = {
-            'delay_secs': test.delay_secs,
-            'time': time.time() + test.delay_secs,
-            'operation': test.operation,
-            'iteration': iteration,
-            'iterations': test.iterations,
-            }
-
-        # Create a new (threaded) event client since we
-        # don't want to use the event loop for this.
-        with EventClient() as event_client:
-            event_client.post_event(Event(Event.Type.PENDING_SHUTDOWN,
-                                          **pending_shutdown_data))
-            aborted = event_client.wait(
-                lambda event: event.type == Event.Type.CANCEL_SHUTDOWN,
-                timeout=test.delay_secs) is not None
-            if aborted:
-                event_client.post_event(Event(Event.Type.PENDING_SHUTDOWN))
-            return aborted
-
-    def init_states(self):
-        '''
-        Initializes all states on startup.
-        '''
-        for test in self.test_list.get_all_tests():
-            # Make sure the state server knows about all the tests,
-            # defaulting to an untested state.
-            test.update_state(update_parent=False, visible=False)
-
-        var_log_messages = None
-
-        # Any 'active' tests should be marked as failed now.
-        for test in self.test_list.walk():
-            state = test.get_state()
-            if state.status != TestState.ACTIVE:
-                continue
-            if isinstance(test, factory.ShutdownStep):
-                # Shutdown while the test was active - that's good.
-                self.handle_shutdown_complete(test, state)
-            else:
-                # Unexpected shutdown.  Grab /var/log/messages for context.
-                if var_log_messages is None:
-                    try:
-                        var_log_messages = (
-                            utils.var_log_messages_before_reboot())
-                        # Write it to the log, to make it easier to
-                        # correlate with /var/log/messages.
-                        logging.info(
-                            'Unexpected shutdown. '
-                            'Tail of /var/log/messages before last reboot:\n'
-                            '%s', ('\n'.join(
-                                    '    ' + x for x in var_log_messages)))
-                    except:
-                        logging.exception('Unable to grok /var/log/messages')
-                        var_log_messages = []
-
-                error_msg = 'Unexpected shutdown while test was running'
-                self.event_log.Log('end_test',
-                                   path=test.path,
-                                   status=TestState.FAILED,
-                                   invocation=test.get_state().invocation,
-                                   error_msg=error_msg,
-                                   var_log_messages='\n'.join(var_log_messages))
-                test.update_state(
-                    status=TestState.FAILED,
-                    error_msg=error_msg)
-
-    def show_next_active_test(self):
-        '''
-        Rotates to the next visible active test.
-        '''
-        self.reap_completed_tests()
-        active_tests = [
-            t for t in self.test_list.walk()
-            if t.is_leaf() and t.get_state().status == TestState.ACTIVE]
-        if not active_tests:
-            return
-
-        try:
-            next_test = active_tests[
-                (active_tests.index(self.visible_test) + 1) % len(active_tests)]
-        except ValueError:  # visible_test not present in active_tests
-            next_test = active_tests[0]
-
-        self.set_visible_test(next_test)
-
-    def handle_event(self, event):
-        '''
-        Handles an event from the event server.
-        '''
-        handler = self.event_handlers.get(event.type)
-        if handler:
-            handler(event)
-        else:
-            # We don't register handlers for all event types - just ignore
-            # this event.
-            logging.debug('Unbound event type %s', event.type)
-
-    def run_next_test(self):
-        '''
-        Runs the next eligible test (or tests) in self.tests_to_run.
-        '''
-        self.reap_completed_tests()
-        while self.tests_to_run:
-            logging.debug('Tests to run: %s',
-                          [x.path for x in self.tests_to_run])
-
-            test = self.tests_to_run[0]
-
-            if test in self.invocations:
-                logging.info('Next test %s is already running', test.path)
-                self.tests_to_run.popleft()
-                return
-
-            if self.invocations and not (test.backgroundable and all(
-                [x.backgroundable for x in self.invocations])):
-                logging.debug('Waiting for non-backgroundable tests to '
-                              'complete before running %s', test.path)
-                return
-
-            self.tests_to_run.popleft()
-
-            if isinstance(test, factory.ShutdownStep):
-                if os.path.exists(NO_REBOOT_FILE):
-                    test.update_state(
-                        status=TestState.FAILED, increment_count=1,
-                        error_msg=('Skipped shutdown since %s is present' %
-                                   NO_REBOOT_FILE))
-                    continue
-
-                test.update_state(status=TestState.ACTIVE, increment_count=1,
-                                  error_msg='', shutdown_count=0)
-                if self._prompt_cancel_shutdown(test, 1):
-                    self.event_log.Log('reboot_cancelled')
-                    test.update_state(
-                        status=TestState.FAILED, increment_count=1,
-                        error_msg='Shutdown aborted by operator',
-                        shutdown_count=0)
-                    return
-
-                # Save pending test list in the state server
-                self.state_instance.set_shared_data(
-                    'tests_after_shutdown',
-                    [t.path for t in self.tests_to_run])
-                # Save shutdown time
-                self.state_instance.set_shared_data('shutdown_time',
-                                                    time.time())
-
-                with self.env.lock:
-                    self.event_log.Log('shutdown', operation=test.operation)
-                    shutdown_result = self.env.shutdown(test.operation)
-                if shutdown_result:
-                    # That's all, folks!
-                    self.run_queue.put(None)
-                    return
-                else:
-                    # Just pass (e.g., in the chroot).
-                    test.update_state(status=TestState.PASSED)
-                    self.state_instance.set_shared_data(
-                        'tests_after_shutdown', None)
-                    # Send event with no fields to indicate that there is no
-                    # longer a pending shutdown.
-                    self.event_client.post_event(Event(
-                            Event.Type.PENDING_SHUTDOWN))
-                    continue
-
-            invoc = TestInvocation(self, test, on_completion=self.run_next_test)
-            self.invocations[test] = invoc
-            if self.visible_test is None and test.has_ui:
-                self.set_visible_test(test)
-            self.check_connection_manager()
-            invoc.start()
-
-    def check_connection_manager(self):
-        exclusive_tests = [
-            test.path
-            for test in self.invocations
-            if test.is_exclusive(
-                factory.FactoryTest.EXCLUSIVE_OPTIONS.NETWORKING)]
-        if exclusive_tests:
-            # Make sure networking is disabled.
-            if self.network_enabled:
-                logging.info('Disabling network, as requested by %s',
-                             exclusive_tests)
-                self.connection_manager.DisableNetworking()
-                self.network_enabled = False
-        else:
-            # Make sure networking is enabled.
-            if not self.network_enabled:
-                logging.info('Re-enabling network')
-                self.connection_manager.EnableNetworking()
-                self.network_enabled = True
-
-    def run_tests(self, subtrees, untested_only=False):
-        '''
-        Runs tests under subtree.
-
-        The tests are run in order unless one fails (then stops).
-        Backgroundable tests are run simultaneously; when a foreground test is
-        encountered, we wait for all active tests to finish before continuing.
-
-        @param subtrees: Node or nodes containing tests to run (may either be
-            a single test or a list).  Duplicates will be ignored.
-        '''
-        if type(subtrees) != list:
-            subtrees = [subtrees]
-
-        # Nodes we've seen so far, to avoid duplicates.
-        seen = set()
-
-        self.tests_to_run = deque()
-        for subtree in subtrees:
-            for test in subtree.walk():
-                if test in seen:
-                    continue
-                seen.add(test)
-
-                if not test.is_leaf():
-                    continue
-                if (untested_only and
-                    test.get_state().status != TestState.UNTESTED):
-                    continue
-                self.tests_to_run.append(test)
-        self.run_next_test()
-
-    def reap_completed_tests(self):
-        '''
-        Removes completed tests from the set of active tests.
-
-        Also updates the visible test if it was reaped.
-        '''
-        for t, v in dict(self.invocations).iteritems():
-            if v.is_completed():
-                del self.invocations[t]
-
-        if (self.visible_test is None or
-            self.visible_test not in self.invocations):
-            self.set_visible_test(None)
-            # Make the first running test, if any, the visible test
-            for t in self.test_list.walk():
-                if t in self.invocations:
-                    self.set_visible_test(t)
-                    break
-
-    def kill_active_tests(self, abort):
-        '''
-        Kills and waits for all active tests.
-
-        @param abort: True to change state of killed tests to FAILED, False for
-                UNTESTED.
-        '''
-        self.reap_completed_tests()
-        for test, invoc in self.invocations.items():
-            factory.console.info('Killing active test %s...' % test.path)
-            invoc.abort_and_join()
-            factory.console.info('Killed %s' % test.path)
-            del self.invocations[test]
-            if not abort:
-                test.update_state(status=TestState.UNTESTED)
-        self.reap_completed_tests()
-
-    def stop(self):
-        self.kill_active_tests(False)
-        self.run_tests([])
-
-    def abort_active_tests(self):
-        self.kill_active_tests(True)
-
-    def main(self):
-        try:
-            self.init()
-            self.event_log.Log('goofy_init',
-                               success=True)
-        except:
-            if self.event_log:
-                try:
-                    self.event_log.Log('goofy_init',
-                                       success=False,
-                                       trace=traceback.format_exc())
-                except:
-                    pass
-            raise
-
-        self.run()
-
-    def update_system_info(self):
-        '''Updates system info.'''
-        system_info = system.SystemInfo()
-        self.state_instance.set_shared_data('system_info', system_info.__dict__)
-        self.event_client.post_event(Event(Event.Type.SYSTEM_INFO,
-                                           system_info=system_info.__dict__))
-        logging.info('System info: %r', system_info.__dict__)
-
-    def update_factory(self):
-        self.kill_active_tests(False)
-        self.run_tests([])
-
-        try:
-            if updater.TryUpdate(pre_update_hook=self.state_instance.close):
-                self.env.shutdown('reboot')
-        except:
-            factory.console.exception('Unable to update')
-
-    def init(self, args=None, env=None):
-        '''Initializes Goofy.
-
-        Args:
-            args: A list of command-line arguments.  Uses sys.argv if
-                args is None.
-            env: An Environment instance to use (or None to choose
-                FakeChrootEnvironment or DUTEnvironment as appropriate).
-        '''
-        parser = OptionParser()
-        parser.add_option('-v', '--verbose', dest='verbose',
-                          action='store_true',
-                          help='Enable debug logging')
-        parser.add_option('--print_test_list', dest='print_test_list',
-                          metavar='FILE',
-                          help='Read and print test list FILE, and exit')
-        parser.add_option('--restart', dest='restart',
-                          action='store_true',
-                          help='Clear all test state')
-        parser.add_option('--ui', dest='ui', type='choice',
-                          choices=['none', 'gtk', 'chrome'],
-                          default='gtk',
-                          help='UI to use')
-        parser.add_option('--ui_scale_factor', dest='ui_scale_factor',
-                          type='int', default=1,
-                          help=('Factor by which to scale UI '
-                                '(Chrome UI only)'))
-        parser.add_option('--test_list', dest='test_list',
-                          metavar='FILE',
-                          help='Use FILE as test list')
-        (self.options, self.args) = parser.parse_args(args)
-
-        global _inited_logging
-        if not _inited_logging:
-            factory.init_logging('goofy', verbose=self.options.verbose)
-            _inited_logging = True
-        self.event_log = EventLog('goofy')
-
-        if (not suppress_chroot_warning and
-            factory.in_chroot() and
-            self.options.ui == 'gtk' and
-            os.environ.get('DISPLAY') in [None, '', ':0', ':0.0']):
-            # That's not going to work!  Tell the user how to run
-            # this way.
-            logging.warn(GOOFY_IN_CHROOT_WARNING)
-            time.sleep(1)
-
-        if env:
-            self.env = env
-        elif factory.in_chroot():
-            self.env = test_environment.FakeChrootEnvironment()
-            logging.warn(
-                'Using chroot environment: will not actually run autotests')
-        else:
-            self.env = test_environment.DUTEnvironment()
-        self.env.goofy = self
-
-        if self.options.restart:
-            state.clear_state()
-
-        if self.options.print_test_list:
-            print (factory.read_test_list(
-                    self.options.print_test_list,
-                    test_classes=dict(test_steps.__dict__)).
-                   __repr__(recursive=True))
-            return
-
-        if self.options.ui_scale_factor != 1 and utils.in_qemu():
-            logging.warn(
-                'In QEMU; ignoring ui_scale_factor argument')
-            self.options.ui_scale_factor = 1
-
-        logging.info('Started')
-
-        self.start_state_server()
-        self.state_instance.set_shared_data('hwid_cfg', get_hwid_cfg())
-        self.state_instance.set_shared_data('ui_scale_factor',
-                                            self.options.ui_scale_factor)
-        self.last_shutdown_time = (
-            self.state_instance.get_shared_data('shutdown_time', optional=True))
-        self.state_instance.del_shared_data('shutdown_time', optional=True)
+      if isinstance(test, factory.ShutdownStep):
+        if os.path.exists(NO_REBOOT_FILE):
+          test.update_state(
+            status=TestState.FAILED, increment_count=1,
+            error_msg=('Skipped shutdown since %s is present' %
+                   NO_REBOOT_FILE))
+          continue
 
-        if not self.options.test_list:
-            self.options.test_list = find_test_list()
-            if not self.options.test_list:
-                logging.error('No test list. Aborting.')
-                sys.exit(1)
-            logging.info('Using test list %s', self.options.test_list)
+        test.update_state(status=TestState.ACTIVE, increment_count=1,
+                  error_msg='', shutdown_count=0)
+        if self._prompt_cancel_shutdown(test, 1):
+          self.event_log.Log('reboot_cancelled')
+          test.update_state(
+            status=TestState.FAILED, increment_count=1,
+            error_msg='Shutdown aborted by operator',
+            shutdown_count=0)
+          return
 
-        self.test_list = factory.read_test_list(
-            self.options.test_list,
-            self.state_instance,
-            test_classes=dict(test_steps.__dict__))
-        if not self.state_instance.has_shared_data('ui_lang'):
-            self.state_instance.set_shared_data('ui_lang',
-                                                self.test_list.options.ui_lang)
+        # Save pending test list in the state server
         self.state_instance.set_shared_data(
-            'test_list_options',
-            self.test_list.options.__dict__)
-        self.state_instance.test_list = self.test_list
+          'tests_after_shutdown',
+          [t.path for t in self.tests_to_run])
+        # Save shutdown time
+        self.state_instance.set_shared_data('shutdown_time',
+                          time.time())
 
-        self.init_states()
-        self.start_event_server()
-        self.connection_manager = self.env.create_connection_manager(
-            self.test_list.options.wlans)
-        # Note that we create a log watcher even if
-        # sync_event_log_period_secs isn't set (no background
-        # syncing), since we may use it to flush event logs as well.
-        self.log_watcher = EventLogWatcher(
-            self.test_list.options.sync_event_log_period_secs,
-            handle_event_logs_callback=self._handle_event_logs)
-        if self.test_list.options.sync_event_log_period_secs:
-            self.log_watcher.StartWatchThread()
+        with self.env.lock:
+          self.event_log.Log('shutdown', operation=test.operation)
+          shutdown_result = self.env.shutdown(test.operation)
+        if shutdown_result:
+          # That's all, folks!
+          self.run_queue.put(None)
+          return
+        else:
+          # Just pass (e.g., in the chroot).
+          test.update_state(status=TestState.PASSED)
+          self.state_instance.set_shared_data(
+            'tests_after_shutdown', None)
+          # Send event with no fields to indicate that there is no
+          # longer a pending shutdown.
+          self.event_client.post_event(Event(
+              Event.Type.PENDING_SHUTDOWN))
+          continue
 
-        self.update_system_info()
+      invoc = TestInvocation(self, test, on_completion=self.run_next_test)
+      self.invocations[test] = invoc
+      if self.visible_test is None and test.has_ui:
+        self.set_visible_test(test)
+      self.check_connection_manager()
+      invoc.start()
 
-        os.environ['CROS_FACTORY'] = '1'
-        os.environ['CROS_DISABLE_SITE_SYSINFO'] = '1'
+  def check_connection_manager(self):
+    exclusive_tests = [
+      test.path
+      for test in self.invocations
+      if test.is_exclusive(
+        factory.FactoryTest.EXCLUSIVE_OPTIONS.NETWORKING)]
+    if exclusive_tests:
+      # Make sure networking is disabled.
+      if self.network_enabled:
+        logging.info('Disabling network, as requested by %s',
+               exclusive_tests)
+        self.connection_manager.DisableNetworking()
+        self.network_enabled = False
+    else:
+      # Make sure networking is enabled.
+      if not self.network_enabled:
+        logging.info('Re-enabling network')
+        self.connection_manager.EnableNetworking()
+        self.network_enabled = True
 
-        # Set CROS_UI since some behaviors in ui.py depend on the
-        # particular UI in use.  TODO(jsalz): Remove this (and all
-        # places it is used) when the GTK UI is removed.
-        os.environ['CROS_UI'] = self.options.ui
+  def run_tests(self, subtrees, untested_only=False):
+    '''
+    Runs tests under subtree.
 
-        if self.options.ui == 'chrome':
-            self.env.launch_chrome()
-            logging.info('Waiting for a web socket connection')
-            self.web_socket_manager.wait()
+    The tests are run in order unless one fails (then stops).
+    Backgroundable tests are run simultaneously; when a foreground test is
+    encountered, we wait for all active tests to finish before continuing.
 
-            # Wait for the test widget size to be set; this is done in
-            # an asynchronous RPC so there is a small chance that the
-            # web socket might be opened first.
-            for i in range(100):  # 10 s
-                try:
-                    if self.state_instance.get_shared_data('test_widget_size'):
-                        break
-                except KeyError:
-                    pass  # Retry
-                time.sleep(0.1)  # 100 ms
-            else:
-                logging.warn('Never received test_widget_size from UI')
-        elif self.options.ui == 'gtk':
-            self.start_ui()
+    @param subtrees: Node or nodes containing tests to run (may either be
+      a single test or a list).  Duplicates will be ignored.
+    '''
+    if type(subtrees) != list:
+      subtrees = [subtrees]
 
-        for handler in self.on_ui_startup:
-            handler()
+    # Nodes we've seen so far, to avoid duplicates.
+    seen = set()
 
-        self.prespawner = Prespawner()
-        self.prespawner.start()
+    self.tests_to_run = deque()
+    for subtree in subtrees:
+      for test in subtree.walk():
+        if test in seen:
+          continue
+        seen.add(test)
 
-        def state_change_callback(test, state):
-            self.event_client.post_event(
-                Event(Event.Type.STATE_CHANGE,
-                      path=test.path, state=state))
-        self.test_list.state_change_callback = state_change_callback
+        if not test.is_leaf():
+          continue
+        if (untested_only and
+          test.get_state().status != TestState.UNTESTED):
+          continue
+        self.tests_to_run.append(test)
+    self.run_next_test()
 
+  def reap_completed_tests(self):
+    '''
+    Removes completed tests from the set of active tests.
+
+    Also updates the visible test if it was reaped.
+    '''
+    for t, v in dict(self.invocations).iteritems():
+      if v.is_completed():
+        del self.invocations[t]
+
+    if (self.visible_test is None or
+      self.visible_test not in self.invocations):
+      self.set_visible_test(None)
+      # Make the first running test, if any, the visible test
+      for t in self.test_list.walk():
+        if t in self.invocations:
+          self.set_visible_test(t)
+          break
+
+  def kill_active_tests(self, abort):
+    '''
+    Kills and waits for all active tests.
+
+    @param abort: True to change state of killed tests to FAILED, False for
+        UNTESTED.
+    '''
+    self.reap_completed_tests()
+    for test, invoc in self.invocations.items():
+      factory.console.info('Killing active test %s...' % test.path)
+      invoc.abort_and_join()
+      factory.console.info('Killed %s' % test.path)
+      del self.invocations[test]
+      if not abort:
+        test.update_state(status=TestState.UNTESTED)
+    self.reap_completed_tests()
+
+  def stop(self):
+    self.kill_active_tests(False)
+    self.run_tests([])
+
+  def abort_active_tests(self):
+    self.kill_active_tests(True)
+
+  def main(self):
+    try:
+      self.init()
+      self.event_log.Log('goofy_init',
+                 success=True)
+    except:
+      if self.event_log:
         try:
-            tests_after_shutdown = self.state_instance.get_shared_data(
-                'tests_after_shutdown')
+          self.event_log.Log('goofy_init',
+                     success=False,
+                     trace=traceback.format_exc())
+        except:  # pylint: disable=W0702
+          pass
+      raise
+
+    self.run()
+
+  def update_system_info(self):
+    '''Updates system info.'''
+    system_info = system.SystemInfo()
+    self.state_instance.set_shared_data('system_info', system_info.__dict__)
+    self.event_client.post_event(Event(Event.Type.SYSTEM_INFO,
+                       system_info=system_info.__dict__))
+    logging.info('System info: %r', system_info.__dict__)
+
+  def update_factory(self):
+    self.kill_active_tests(False)
+    self.run_tests([])
+
+    try:
+      if updater.TryUpdate(pre_update_hook=self.state_instance.close):
+        self.env.shutdown('reboot')
+    except:  # pylint: disable=W0702
+      factory.console.exception('Unable to update')
+
+  def init(self, args=None, env=None):
+    '''Initializes Goofy.
+
+    Args:
+      args: A list of command-line arguments.  Uses sys.argv if
+        args is None.
+      env: An Environment instance to use (or None to choose
+        FakeChrootEnvironment or DUTEnvironment as appropriate).
+    '''
+    parser = OptionParser()
+    parser.add_option('-v', '--verbose', dest='verbose',
+              action='store_true',
+              help='Enable debug logging')
+    parser.add_option('--print_test_list', dest='print_test_list',
+              metavar='FILE',
+              help='Read and print test list FILE, and exit')
+    parser.add_option('--restart', dest='restart',
+              action='store_true',
+              help='Clear all test state')
+    parser.add_option('--ui', dest='ui', type='choice',
+              choices=['none', 'gtk', 'chrome'],
+              default='gtk',
+              help='UI to use')
+    parser.add_option('--ui_scale_factor', dest='ui_scale_factor',
+              type='int', default=1,
+              help=('Factor by which to scale UI '
+                '(Chrome UI only)'))
+    parser.add_option('--test_list', dest='test_list',
+              metavar='FILE',
+              help='Use FILE as test list')
+    (self.options, self.args) = parser.parse_args(args)
+
+    global _inited_logging  # pylint: disable=W0603
+    if not _inited_logging:
+      factory.init_logging('goofy', verbose=self.options.verbose)
+      _inited_logging = True
+    self.event_log = EventLog('goofy')
+
+    if (not suppress_chroot_warning and
+      factory.in_chroot() and
+      self.options.ui == 'gtk' and
+      os.environ.get('DISPLAY') in [None, '', ':0', ':0.0']):
+      # That's not going to work!  Tell the user how to run
+      # this way.
+      logging.warn(GOOFY_IN_CHROOT_WARNING)
+      time.sleep(1)
+
+    if env:
+      self.env = env
+    elif factory.in_chroot():
+      self.env = test_environment.FakeChrootEnvironment()
+      logging.warn(
+        'Using chroot environment: will not actually run autotests')
+    else:
+      self.env = test_environment.DUTEnvironment()
+    self.env.goofy = self
+
+    if self.options.restart:
+      state.clear_state()
+
+    if self.options.print_test_list:
+      print (factory.read_test_list(
+          self.options.print_test_list,
+          test_classes=dict(test_steps.__dict__)).
+           __repr__(recursive=True))
+      return
+
+    if self.options.ui_scale_factor != 1 and utils.in_qemu():
+      logging.warn(
+        'In QEMU; ignoring ui_scale_factor argument')
+      self.options.ui_scale_factor = 1
+
+    logging.info('Started')
+
+    self.start_state_server()
+    self.state_instance.set_shared_data('hwid_cfg', get_hwid_cfg())
+    self.state_instance.set_shared_data('ui_scale_factor',
+                      self.options.ui_scale_factor)
+    self.last_shutdown_time = (
+      self.state_instance.get_shared_data('shutdown_time', optional=True))
+    self.state_instance.del_shared_data('shutdown_time', optional=True)
+
+    if not self.options.test_list:
+      self.options.test_list = find_test_list()
+      if not self.options.test_list:
+        logging.error('No test list. Aborting.')
+        sys.exit(1)
+      logging.info('Using test list %s', self.options.test_list)
+
+    self.test_list = factory.read_test_list(
+      self.options.test_list,
+      self.state_instance,
+      test_classes=dict(test_steps.__dict__))
+    if not self.state_instance.has_shared_data('ui_lang'):
+      self.state_instance.set_shared_data('ui_lang',
+                        self.test_list.options.ui_lang)
+    self.state_instance.set_shared_data(
+      'test_list_options',
+      self.test_list.options.__dict__)
+    self.state_instance.test_list = self.test_list
+
+    self.init_states()
+    self.start_event_server()
+    self.connection_manager = self.env.create_connection_manager(
+      self.test_list.options.wlans)
+    # Note that we create a log watcher even if
+    # sync_event_log_period_secs isn't set (no background
+    # syncing), since we may use it to flush event logs as well.
+    self.log_watcher = EventLogWatcher(
+      self.test_list.options.sync_event_log_period_secs,
+      handle_event_logs_callback=self._handle_event_logs)
+    if self.test_list.options.sync_event_log_period_secs:
+      self.log_watcher.StartWatchThread()
+
+    self.update_system_info()
+
+    os.environ['CROS_FACTORY'] = '1'
+    os.environ['CROS_DISABLE_SITE_SYSINFO'] = '1'
+
+    # Set CROS_UI since some behaviors in ui.py depend on the
+    # particular UI in use.  TODO(jsalz): Remove this (and all
+    # places it is used) when the GTK UI is removed.
+    os.environ['CROS_UI'] = self.options.ui
+
+    if self.options.ui == 'chrome':
+      self.env.launch_chrome()
+      logging.info('Waiting for a web socket connection')
+      self.web_socket_manager.wait()
+
+      # Wait for the test widget size to be set; this is done in
+      # an asynchronous RPC so there is a small chance that the
+      # web socket might be opened first.
+      for _ in range(100):  # 10 s
+        try:
+          if self.state_instance.get_shared_data('test_widget_size'):
+            break
         except KeyError:
-            tests_after_shutdown = None
+          pass  # Retry
+        time.sleep(0.1)  # 100 ms
+      else:
+        logging.warn('Never received test_widget_size from UI')
+    elif self.options.ui == 'gtk':
+      self.start_ui()
 
-        if tests_after_shutdown is not None:
-            logging.info('Resuming tests after shutdown: %s',
-                         tests_after_shutdown)
-            self.state_instance.set_shared_data('tests_after_shutdown', None)
-            self.tests_to_run.extend(
-                self.test_list.lookup_path(t) for t in tests_after_shutdown)
-            self.run_queue.put(self.run_next_test)
-        else:
-            if self.test_list.options.auto_run_on_start:
-                self.run_queue.put(
-                    lambda: self.run_tests(self.test_list, untested_only=True))
+    for handler in self.on_ui_startup:
+      handler()
 
-    def run(self):
-        '''Runs Goofy.'''
-        # Process events forever.
-        while self.run_once(True):
-            pass
+    self.prespawner = Prespawner()
+    self.prespawner.start()
 
-    def run_once(self, block=False):
-        '''Runs all items pending in the event loop.
+    def state_change_callback(test, test_state):
+      self.event_client.post_event(
+        Event(Event.Type.STATE_CHANGE,
+            path=test.path, state=test_state))
+    self.test_list.state_change_callback = state_change_callback
 
-        Args:
-            block: If true, block until at least one event is processed.
+    try:
+      tests_after_shutdown = self.state_instance.get_shared_data(
+        'tests_after_shutdown')
+    except KeyError:
+      tests_after_shutdown = None
 
-        Returns:
-            True to keep going or False to shut down.
-        '''
-        events = utils.DrainQueue(self.run_queue)
-        if not events:
-            # Nothing on the run queue.
-            self._run_queue_idle()
-            if block:
-                # Block for at least one event...
-                events.append(self.run_queue.get())
-                # ...and grab anything else that showed up at the same
-                # time.
-                events.extend(utils.DrainQueue(self.run_queue))
+    if tests_after_shutdown is not None:
+      logging.info('Resuming tests after shutdown: %s',
+             tests_after_shutdown)
+      self.state_instance.set_shared_data('tests_after_shutdown', None)
+      self.tests_to_run.extend(
+        self.test_list.lookup_path(t) for t in tests_after_shutdown)
+      self.run_queue.put(self.run_next_test)
+    else:
+      if self.test_list.options.auto_run_on_start:
+        self.run_queue.put(
+          lambda: self.run_tests(self.test_list, untested_only=True))
 
-        for event in events:
-            if not event:
-                # Shutdown request.
-                self.run_queue.task_done()
-                return False
+  def run(self):
+    '''Runs Goofy.'''
+    # Process events forever.
+    while self.run_once(True):
+      pass
 
-            try:
-                event()
-            except Exception as e:  # pylint: disable=W0703
-                logging.error('Error in event loop: %s', e)
-                traceback.print_exc(sys.stderr)
-                self.record_exception(traceback.format_exception_only(
-                        *sys.exc_info()[:2]))
-                # But keep going
-            finally:
-                self.run_queue.task_done()
-        return True
+  def run_once(self, block=False):
+    '''Runs all items pending in the event loop.
 
-    def _run_queue_idle(self):
-        '''Invoked when the run queue has no events.'''
-        self.check_connection_manager()
+    Args:
+      block: If true, block until at least one event is processed.
 
-    def _handle_event_logs(self, log_name, chunk):
-        '''Callback for event watcher.
+    Returns:
+      True to keep going or False to shut down.
+    '''
+    events = utils.DrainQueue(self.run_queue)
+    if not events:
+      # Nothing on the run queue.
+      self._run_queue_idle()
+      if block:
+        # Block for at least one event...
+        events.append(self.run_queue.get())
+        # ...and grab anything else that showed up at the same
+        # time.
+        events.extend(utils.DrainQueue(self.run_queue))
 
-        Attempts to upload the event logs to the shopfloor server.
-        '''
-        description = 'event logs (%s, %d bytes)' % (log_name, len(chunk))
-        start_time = time.time()
-        logging.info('Syncing %s', description)
-        shopfloor_client = shopfloor.get_instance(
-            detect=True,
-            timeout=self.test_list.options.shopfloor_timeout_secs)
-        shopfloor_client.UploadEvent(log_name, chunk)
-        logging.info(
-            'Successfully synced %s in %.03f s',
-            description, time.time() - start_time)
+    for event in events:
+      if not event:
+        # Shutdown request.
+        self.run_queue.task_done()
+        return False
 
-    def run_tests_with_status(self, statuses_to_run, starting_at=None,
-        root=None):
-        '''Runs all top-level tests with a particular status.
+      try:
+        event()
+      except Exception as e:  # pylint: disable=W0703
+        logging.error('Error in event loop: %s', e)
+        traceback.print_exc(sys.stderr)
+        self.record_exception(traceback.format_exception_only(
+            *sys.exc_info()[:2]))
+        # But keep going
+      finally:
+        self.run_queue.task_done()
+    return True
 
-        All active tests, plus any tests to re-run, are reset.
+  def _run_queue_idle(self):
+    '''Invoked when the run queue has no events.'''
+    self.check_connection_manager()
 
-        Args:
-            starting_at: If provided, only auto-runs tests beginning with
-                this test.
-        '''
-        root = root or self.test_list
+  def _handle_event_logs(self, log_name, chunk):
+    '''Callback for event watcher.
 
-        if starting_at:
-            # Make sure they passed a test, not a string.
-            assert isinstance(starting_at, factory.FactoryTest)
+    Attempts to upload the event logs to the shopfloor server.
+    '''
+    description = 'event logs (%s, %d bytes)' % (log_name, len(chunk))
+    start_time = time.time()
+    logging.info('Syncing %s', description)
+    shopfloor_client = shopfloor.get_instance(
+      detect=True,
+      timeout=self.test_list.options.shopfloor_timeout_secs)
+    shopfloor_client.UploadEvent(log_name, chunk)
+    logging.info(
+      'Successfully synced %s in %.03f s',
+      description, time.time() - start_time)
 
-        tests_to_reset = []
-        tests_to_run = []
+  def run_tests_with_status(self, statuses_to_run, starting_at=None,
+    root=None):
+    '''Runs all top-level tests with a particular status.
 
-        found_starting_at = False
+    All active tests, plus any tests to re-run, are reset.
 
-        for test in root.get_top_level_tests():
-            if starting_at:
-                if test == starting_at:
-                    # We've found starting_at; do auto-run on all
-                    # subsequent tests.
-                    found_starting_at = True
-                if not found_starting_at:
-                    # Don't start this guy yet
-                    continue
+    Args:
+      starting_at: If provided, only auto-runs tests beginning with
+        this test.
+    '''
+    root = root or self.test_list
 
-            status = test.get_state().status
-            if status == TestState.ACTIVE or status in statuses_to_run:
-                # Reset the test (later; we will need to abort
-                # all active tests first).
-                tests_to_reset.append(test)
-            if status in statuses_to_run:
-                tests_to_run.append(test)
+    if starting_at:
+      # Make sure they passed a test, not a string.
+      assert isinstance(starting_at, factory.FactoryTest)
 
-        self.abort_active_tests()
+    tests_to_reset = []
+    tests_to_run = []
 
-        # Reset all statuses of the tests to run (in case any tests were active;
-        # we want them to be run again).
-        for test_to_reset in tests_to_reset:
-            for test in test_to_reset.walk():
-                test.update_state(status=TestState.UNTESTED)
+    found_starting_at = False
 
-        self.run_tests(tests_to_run, untested_only=True)
+    for test in root.get_top_level_tests():
+      if starting_at:
+        if test == starting_at:
+          # We've found starting_at; do auto-run on all
+          # subsequent tests.
+          found_starting_at = True
+        if not found_starting_at:
+          # Don't start this guy yet
+          continue
 
-    def restart_tests(self, root=None):
-        '''Restarts all tests.'''
-        root = root or self.test_list
+      status = test.get_state().status
+      if status == TestState.ACTIVE or status in statuses_to_run:
+        # Reset the test (later; we will need to abort
+        # all active tests first).
+        tests_to_reset.append(test)
+      if status in statuses_to_run:
+        tests_to_run.append(test)
 
-        self.abort_active_tests()
-        for test in root.walk():
-            test.update_state(status=TestState.UNTESTED)
-        self.run_tests(root)
+    self.abort_active_tests()
 
-    def auto_run(self, starting_at=None, root=None):
-        '''"Auto-runs" tests that have not been run yet.
+    # Reset all statuses of the tests to run (in case any tests were active;
+    # we want them to be run again).
+    for test_to_reset in tests_to_reset:
+      for test in test_to_reset.walk():
+        test.update_state(status=TestState.UNTESTED)
 
-        Args:
-            starting_at: If provide, only auto-runs tests beginning with
-                this test.
-        '''
-        root = root or self.test_list
-        self.run_tests_with_status([TestState.UNTESTED, TestState.ACTIVE],
-                                   starting_at=starting_at,
-                                   root=root)
+    self.run_tests(tests_to_run, untested_only=True)
 
-    def re_run_failed(self, root=None):
-        '''Re-runs failed tests.'''
-        root = root or self.test_list
-        self.run_tests_with_status([TestState.FAILED], root=root)
+  def restart_tests(self, root=None):
+    '''Restarts all tests.'''
+    root = root or self.test_list
 
-    def show_review_information(self):
-        '''Event handler for showing review information screen.
+    self.abort_active_tests()
+    for test in root.walk():
+      test.update_state(status=TestState.UNTESTED)
+    self.run_tests(root)
 
-        The information screene is rendered by main UI program (ui.py), so in
-        goofy we only need to kill all active tests, set them as untested, and
-        clear remaining tests.
-        '''
-        self.kill_active_tests(False)
-        self.run_tests([])
+  def auto_run(self, starting_at=None, root=None):
+    '''"Auto-runs" tests that have not been run yet.
 
-    def handle_switch_test(self, event):
-        '''Switches to a particular test.
+    Args:
+      starting_at: If provide, only auto-runs tests beginning with
+        this test.
+    '''
+    root = root or self.test_list
+    self.run_tests_with_status([TestState.UNTESTED, TestState.ACTIVE],
+                   starting_at=starting_at,
+                   root=root)
 
-        @param event: The SWITCH_TEST event.
-        '''
-        test = self.test_list.lookup_path(event.path)
-        if not test:
-            logging.error('Unknown test %r', event.key)
-            return
+  def re_run_failed(self, root=None):
+    '''Re-runs failed tests.'''
+    root = root or self.test_list
+    self.run_tests_with_status([TestState.FAILED], root=root)
 
-        invoc = self.invocations.get(test)
-        if invoc and test.backgroundable:
-            # Already running: just bring to the front if it
-            # has a UI.
-            logging.info('Setting visible test to %s', test.path)
-            self.event_client.post_event(
-                Event(Event.Type.SET_VISIBLE_TEST, path=test.path))
-            return
+  def show_review_information(self):
+    '''Event handler for showing review information screen.
 
-        self.abort_active_tests()
-        for t in test.walk():
-            t.update_state(status=TestState.UNTESTED)
+    The information screene is rendered by main UI program (ui.py), so in
+    goofy we only need to kill all active tests, set them as untested, and
+    clear remaining tests.
+    '''
+    self.kill_active_tests(False)
+    self.run_tests([])
 
-        if self.test_list.options.auto_run_on_keypress:
-            self.auto_run(starting_at=test)
-        else:
-            self.run_tests(test)
+  def handle_switch_test(self, event):
+    '''Switches to a particular test.
 
-    def wait(self):
-        '''Waits for all pending invocations.
+    @param event: The SWITCH_TEST event.
+    '''
+    test = self.test_list.lookup_path(event.path)
+    if not test:
+      logging.error('Unknown test %r', event.key)
+      return
 
-        Useful for testing.
-        '''
-        for k, v in self.invocations.iteritems():
-            logging.info('Waiting for %s to complete...', k)
-            v.thread.join()
+    invoc = self.invocations.get(test)
+    if invoc and test.backgroundable:
+      # Already running: just bring to the front if it
+      # has a UI.
+      logging.info('Setting visible test to %s', test.path)
+      self.event_client.post_event(
+        Event(Event.Type.SET_VISIBLE_TEST, path=test.path))
+      return
 
-    def check_exceptions(self):
-        '''Raises an error if any exceptions have occurred in
-        invocation threads.'''
-        if self.exceptions:
-            raise RuntimeError('Exception in invocation thread: %r' %
-                               self.exceptions)
+    self.abort_active_tests()
+    for t in test.walk():
+      t.update_state(status=TestState.UNTESTED)
 
-    def record_exception(self, msg):
-        '''Records an exception in an invocation thread.
+    if self.test_list.options.auto_run_on_keypress:
+      self.auto_run(starting_at=test)
+    else:
+      self.run_tests(test)
 
-        An exception with the given message will be rethrown when
-        Goofy is destroyed.'''
-        self.exceptions.append(msg)
+  def wait(self):
+    '''Waits for all pending invocations.
+
+    Useful for testing.
+    '''
+    for k, v in self.invocations.iteritems():
+      logging.info('Waiting for %s to complete...', k)
+      v.thread.join()
+
+  def check_exceptions(self):
+    '''Raises an error if any exceptions have occurred in
+    invocation threads.'''
+    if self.exceptions:
+      raise RuntimeError('Exception in invocation thread: %r' %
+                 self.exceptions)
+
+  def record_exception(self, msg):
+    '''Records an exception in an invocation thread.
+
+    An exception with the given message will be rethrown when
+    Goofy is destroyed.'''
+    self.exceptions.append(msg)
 
 
 if __name__ == '__main__':
-    Goofy().main()
+  Goofy().main()
diff --git a/py/goofy/goofy_unittest.py b/py/goofy/goofy_unittest.py
index 0b7da59..c0243dd 100755
--- a/py/goofy/goofy_unittest.py
+++ b/py/goofy/goofy_unittest.py
@@ -6,7 +6,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import factory_common
+import factory_common  # pylint: disable=W0611
 
 import logging
 import math
@@ -18,7 +18,6 @@
 import threading
 import time
 import unittest
-from Queue import Queue
 
 from mox import IgnoreArg
 from ws4py.client import WebSocketBaseClient
@@ -30,395 +29,405 @@
 from cros.factory.test.event import Event
 from cros.factory.goofy.goofy import Goofy
 from cros.factory.goofy.connection_manager \
-    import ConnectionManager
+  import ConnectionManager
 from cros.factory.goofy.test_environment import Environment
 
 
 def init_goofy(env=None, test_list=None, options='', restart=True, ui='none'):
-    '''Initializes and returns a Goofy.'''
-    goofy = Goofy()
-    args = ['--ui', ui]
-    if restart:
-        args.append('--restart')
-    if test_list:
-        out = tempfile.NamedTemporaryFile(prefix='test_list', delete=False)
+  '''Initializes and returns a Goofy.'''
+  new_goofy = Goofy()
+  args = ['--ui', ui]
+  if restart:
+    args.append('--restart')
+  if test_list:
+    out = tempfile.NamedTemporaryFile(prefix='test_list', delete=False)
 
-        # Remove whitespace at the beginning of each line of options.
-        options = re.sub('(?m)^\s+', '', options)
-        out.write('TEST_LIST = [' + test_list + ']\n' + options)
-        out.close()
-        args.extend(['--test_list', out.name])
-    logging.info('Running goofy with args %r', args)
-    goofy.init(args, env or Environment())
-    return goofy
+    # Remove whitespace at the beginning of each line of options.
+    options = re.sub('(?m)^\s+', '', options)
+    out.write('TEST_LIST = [' + test_list + ']\n' + options)
+    out.close()
+    args.extend(['--test_list', out.name])
+  logging.info('Running goofy with args %r', args)
+  new_goofy.init(args, env or Environment())
+  return new_goofy
 
 
 def mock_autotest(env, name, passed, error_msg):
-    '''Adds a side effect that a mock autotest will be executed.
+  '''Adds a side effect that a mock autotest will be executed.
 
-    Args:
-        name: The name of the autotest to be mocked.
-        passed: Whether the test should pass.
-        error_msg: The error message.
-    '''
-    def side_effect(name, args, env_additions, result_file):
-        with open(result_file, 'w') as out:
-            pickle.dump((passed, error_msg), out)
-            return subprocess.Popen(['true'])
+  Args:
+    name: The name of the autotest to be mocked.
+    passed: Whether the test should pass.
+    error_msg: The error message.
+  '''
+  def side_effect(dummy_name, dummy_args, dummy_env_additions,
+          result_file):
+    with open(result_file, 'w') as out:
+      pickle.dump((passed, error_msg), out)
+      return subprocess.Popen(['true'])
 
-    env.spawn_autotest(
-        name, IgnoreArg(), IgnoreArg(), IgnoreArg()).WithSideEffects(
-        side_effect)
+  env.spawn_autotest(
+    name, IgnoreArg(), IgnoreArg(), IgnoreArg()).WithSideEffects(
+    side_effect)
 
 
 class GoofyTest(unittest.TestCase):
-    '''Base class for Goofy test cases.'''
-    options = ''
-    ui = 'none'
-    expected_create_connection_manager_arg = []
+  '''Base class for Goofy test cases.'''
+  options = ''
+  ui = 'none'
+  expected_create_connection_manager_arg = []
+  test_list = None  # Overridden by subclasses
 
-    def setUp(self):
-        self.mocker = mox.Mox()
-        self.env = self.mocker.CreateMock(Environment)
-        self.state = state.get_instance()
-        self.connection_manager = self.mocker.CreateMock(ConnectionManager)
-        self.env.create_connection_manager(
-            self.expected_create_connection_manager_arg).AndReturn(
-            self.connection_manager)
-        self.before_init_goofy()
-        self.mocker.ReplayAll()
-        self.goofy = init_goofy(self.env, self.test_list, self.options,
-                                ui=self.ui)
-        self.mocker.VerifyAll()
-        self.mocker.ResetAll()
+  def setUp(self):
+    self.mocker = mox.Mox()
+    self.env = self.mocker.CreateMock(Environment)
+    self.state = state.get_instance()
+    self.connection_manager = self.mocker.CreateMock(ConnectionManager)
+    self.env.create_connection_manager(
+      self.expected_create_connection_manager_arg).AndReturn(
+      self.connection_manager)
+    self.before_init_goofy()
+    self.mocker.ReplayAll()
+    self.goofy = init_goofy(self.env, self.test_list, self.options,
+                ui=self.ui)
+    self.mocker.VerifyAll()
+    self.mocker.ResetAll()
 
-    def tearDown(self):
-        self.goofy.destroy()
+  def tearDown(self):
+    self.goofy.destroy()
 
-        # Make sure we're not leaving any extra threads hanging around
-        # after a second.
-        for _ in range(10):
-            extra_threads = [t for t in threading.enumerate()
-                             if t != threading.current_thread()]
-            if not extra_threads:
-                break
-            logging.info('Waiting for %d threads to die', len(extra_threads))
+    # Make sure we're not leaving any extra threads hanging around
+    # after a second.
+    for _ in range(10):
+      extra_threads = [t for t in threading.enumerate()
+               if t != threading.current_thread()]
+      if not extra_threads:
+        break
+      logging.info('Waiting for %d threads to die', len(extra_threads))
 
-            # Wait another 100 ms
-            time.sleep(.1)
+      # Wait another 100 ms
+      time.sleep(.1)
 
-        self.assertEqual([], extra_threads)
+    self.assertEqual([], extra_threads)
 
-    def _wait(self):
-        '''Waits for any pending invocations in Goofy to complete,
-        and verifies and resets all mocks.'''
-        self.goofy.wait()
-        self.mocker.VerifyAll()
-        self.mocker.ResetAll()
+  def _wait(self):
+    '''Waits for any pending invocations in Goofy to complete,
+    and verifies and resets all mocks.'''
+    self.goofy.wait()
+    self.mocker.VerifyAll()
+    self.mocker.ResetAll()
 
-    def before_init_goofy(self):
-        '''Hook invoked before init_goofy.'''
+  def before_init_goofy(self):
+    '''Hook invoked before init_goofy.'''
 
-    def check_one_test(self, id, name, passed, error_msg, trigger=None):
-        '''Runs a single autotest, waiting for it to complete.
+  def check_one_test(self, test_id, name, passed, error_msg, trigger=None):
+    '''Runs a single autotest, waiting for it to complete.
 
-        Args:
-            id: The ID of the test expected to run.
-            name: The autotest name of the test expected to run.
-            passed: Whether the test should pass.
-            error_msg: The error message, if any.
-            trigger: An optional callable that will be executed after mocks are
-                set up to trigger the autotest.  If None, then the test is
-                expected to start itself.
-        '''
-        mock_autotest(self.env, name, passed, error_msg)
-        self.mocker.ReplayAll()
-        if trigger:
-            trigger()
-        self.assertTrue(self.goofy.run_once())
-        self.assertEqual([id],
-                         [test.path for test in self.goofy.invocations])
-        self._wait()
-        state = self.state.get_test_state(id)
-        self.assertEqual(TestState.PASSED if passed else TestState.FAILED,
-                         state.status)
-        self.assertEqual(1, state.count)
-        self.assertEqual(error_msg, state.error_msg)
+    Args:
+      test_id: The ID of the test expected to run.
+      name: The autotest name of the test expected to run.
+      passed: Whether the test should pass.
+      error_msg: The error message, if any.
+      trigger: An optional callable that will be executed after mocks are
+        set up to trigger the autotest.  If None, then the test is
+        expected to start itself.
+    '''
+    mock_autotest(self.env, name, passed, error_msg)
+    self.mocker.ReplayAll()
+    if trigger:
+      trigger()
+    self.assertTrue(self.goofy.run_once())
+    self.assertEqual([test_id],
+             [test.path for test in self.goofy.invocations])
+    self._wait()
+    test_state = self.state.get_test_state(test_id)
+    self.assertEqual(TestState.PASSED if passed else TestState.FAILED,
+             test_state.status)
+    self.assertEqual(1, test_state.count)
+    self.assertEqual(error_msg, test_state.error_msg)
 
 
 # A simple test list with three tests.
 ABC_TEST_LIST = '''
-    OperatorTest(id='a', autotest_name='a_A'),
-    OperatorTest(id='b', autotest_name='b_B'),
-    OperatorTest(id='c', autotest_name='c_C'),
+  OperatorTest(id='a', autotest_name='a_A'),
+  OperatorTest(id='b', autotest_name='b_B'),
+  OperatorTest(id='c', autotest_name='c_C'),
 '''
 
 
 class BasicTest(GoofyTest):
-    '''A simple test case that checks that tests are run in the correct
-    order.'''
-    test_list = ABC_TEST_LIST
-    def runTest(self):
-        self.check_one_test('a', 'a_A', True, '')
-        self.check_one_test('b', 'b_B', False, 'Uh-oh')
-        self.check_one_test('c', 'c_C', False, 'Uh-oh')
+  '''A simple test case that checks that tests are run in the correct
+  order.'''
+  test_list = ABC_TEST_LIST
+  def runTest(self):
+    self.check_one_test('a', 'a_A', True, '')
+    self.check_one_test('b', 'b_B', False, 'Uh-oh')
+    self.check_one_test('c', 'c_C', False, 'Uh-oh')
 
 
 class WebSocketTest(GoofyTest):
-    '''A test case that checks the behavior of web sockets.'''
-    test_list = ABC_TEST_LIST
-    ui = 'chrome'
+  '''A test case that checks the behavior of web sockets.'''
+  test_list = ABC_TEST_LIST
+  ui = 'chrome'
 
-    def before_init_goofy(self):
-        # Keep a record of events we received
-        self.events = []
-        # Trigger this event once the web socket closes
-        self.ws_done = threading.Event()
+  def __init__(self, *args, **kwargs):
+    super(WebSocketTest, self).__init__(*args, **kwargs)
+    self.events = None
+    self.ws_done = None
 
-        class MyClient(WebSocketBaseClient):
-            def handshake_ok(socket_self):
-                pass
+  def before_init_goofy(self):
+    # Keep a record of events we received
+    self.events = []
+    # Trigger this event once the web socket closes
+    self.ws_done = threading.Event()
 
-            def received_message(socket_self, message):
-                event = Event.from_json(str(message))
-                logging.info('Test client received %s', event)
-                self.events.append(event)
-                if event.type == Event.Type.HELLO:
-                    socket_self.send(Event(Event.Type.KEEPALIVE,
-                                           uuid=event.uuid).to_json())
+    class MyClient(WebSocketBaseClient):
+      # pylint: disable=E0213
+      def handshake_ok(socket_self):
+        pass
 
-        ws = MyClient(
-            'http://localhost:%d/event' % state.DEFAULT_FACTORY_STATE_PORT,
-            protocols=None, extensions=None)
+      def received_message(socket_self, message):
+        event = Event.from_json(str(message))
+        logging.info('Test client received %s', event)
+        self.events.append(event)
+        if event.type == Event.Type.HELLO:
+          socket_self.send(Event(Event.Type.KEEPALIVE,
+                       uuid=event.uuid).to_json())
 
-        def open_web_socket():
-            ws.connect()
-            # Simulate setting the test widget size/position, since goofy
-            # waits for it.
-            factory.set_shared_data('test_widget_size', [100, 200],
-                                    'test_widget_position', [300, 400])
-            ws.run()
-            self.ws_done.set()
-        self.env.launch_chrome().WithSideEffects(
-            lambda: threading.Thread(target=open_web_socket).start()
-            ).AndReturn(None)
+    ws = MyClient(
+      'http://localhost:%d/event' % state.DEFAULT_FACTORY_STATE_PORT,
+      protocols=None, extensions=None)
 
-    def runTest(self):
-        self.check_one_test('a', 'a_A', True, '')
-        self.check_one_test('b', 'b_B', False, 'Uh-oh')
-        self.check_one_test('c', 'c_C', False, 'Uh-oh')
+    def open_web_socket():
+      ws.connect()
+      # Simulate setting the test widget size/position, since goofy
+      # waits for it.
+      factory.set_shared_data('test_widget_size', [100, 200],
+                  'test_widget_position', [300, 400])
+      ws.run()
+      self.ws_done.set()
+    # pylint: disable=W0108
+    self.env.launch_chrome().WithSideEffects(
+      lambda: threading.Thread(target=open_web_socket).start()
+      ).AndReturn(None)
 
-        # Kill Goofy and wait for the web socket to close gracefully
-        self.goofy.destroy()
-        self.ws_done.wait()
+  def runTest(self):
+    self.check_one_test('a', 'a_A', True, '')
+    self.check_one_test('b', 'b_B', False, 'Uh-oh')
+    self.check_one_test('c', 'c_C', False, 'Uh-oh')
 
-        events_by_type = {}
-        for event in self.events:
-            events_by_type.setdefault(event.type, []).append(event)
+    # Kill Goofy and wait for the web socket to close gracefully
+    self.goofy.destroy()
+    self.ws_done.wait()
 
-        # There should be one hello event
-        self.assertEqual(1, len(events_by_type[Event.Type.HELLO]))
+    events_by_type = {}
+    for event in self.events:
+      events_by_type.setdefault(event.type, []).append(event)
 
-        # There should be at least one log event
-        self.assertTrue(Event.Type.LOG in events_by_type), repr(events_by_type)
+    # There should be one hello event
+    self.assertEqual(1, len(events_by_type[Event.Type.HELLO]))
 
-        # Each test should have a transition to active and to its
-        # final state
-        for path, final_status in (('a', TestState.PASSED),
-                                   ('b', TestState.FAILED),
-                                   ('c', TestState.FAILED)):
-            statuses = [
-                event.state['status']
-                for event in events_by_type[Event.Type.STATE_CHANGE]
-                if event.path == path]
-            self.assertEqual(
-                ['UNTESTED', 'ACTIVE', final_status],
-                statuses)
+    # There should be at least one log event
+    self.assertTrue(Event.Type.LOG in events_by_type,
+            repr(events_by_type))
+
+    # Each test should have a transition to active and to its
+    # final state
+    for path, final_status in (('a', TestState.PASSED),
+                   ('b', TestState.FAILED),
+                   ('c', TestState.FAILED)):
+      statuses = [
+        event.state['status']
+        for event in events_by_type[Event.Type.STATE_CHANGE]
+        if event.path == path]
+      self.assertEqual(
+        ['UNTESTED', 'ACTIVE', final_status],
+        statuses)
 
 
 class ShutdownTest(GoofyTest):
-    test_list = '''
-        RebootStep(id='shutdown', iterations=3),
-        OperatorTest(id='a', autotest_name='a_A')
-    '''
-    def runTest(self):
-        # Expect a reboot request
-        self.env.shutdown('reboot').AndReturn(True)
-        self.mocker.ReplayAll()
-        self.assertTrue(self.goofy.run_once())
-        self._wait()
+  test_list = '''
+    RebootStep(id='shutdown', iterations=3),
+    OperatorTest(id='a', autotest_name='a_A')
+  '''
+  def runTest(self):
+    # Expect a reboot request
+    self.env.shutdown('reboot').AndReturn(True)
+    self.mocker.ReplayAll()
+    self.assertTrue(self.goofy.run_once())
+    self._wait()
 
-        # That should have enqueued a task that will cause Goofy
-        # to shut down.
-        self.mocker.ReplayAll()
-        self.assertFalse(self.goofy.run_once())
-        # There should be a list of tests to run on wake-up.
-        self.assertEqual(
-            ['a'], self.state.get_shared_data('tests_after_shutdown'))
-        self._wait()
+    # That should have enqueued a task that will cause Goofy
+    # to shut down.
+    self.mocker.ReplayAll()
+    self.assertFalse(self.goofy.run_once())
+    # There should be a list of tests to run on wake-up.
+    self.assertEqual(
+      ['a'], self.state.get_shared_data('tests_after_shutdown'))
+    self._wait()
 
-        # Kill and restart Goofy to simulate a shutdown.
-        # Goofy should call for another shutdown.
-        for _ in range(2):
-            self.env.create_connection_manager([]).AndReturn(
-                self.connection_manager)
-            self.env.shutdown('reboot').AndReturn(True)
-            self.mocker.ReplayAll()
-            self.goofy.destroy()
-            self.goofy = init_goofy(self.env, self.test_list, restart=False)
-            self._wait()
+    # Kill and restart Goofy to simulate a shutdown.
+    # Goofy should call for another shutdown.
+    for _ in range(2):
+      self.env.create_connection_manager([]).AndReturn(
+        self.connection_manager)
+      self.env.shutdown('reboot').AndReturn(True)
+      self.mocker.ReplayAll()
+      self.goofy.destroy()
+      self.goofy = init_goofy(self.env, self.test_list, restart=False)
+      self._wait()
 
-        # No more shutdowns - now 'a' should run.
-        self.check_one_test('a', 'a_A', True, '')
+    # No more shutdowns - now 'a' should run.
+    self.check_one_test('a', 'a_A', True, '')
 
 
 class RebootFailureTest(GoofyTest):
-    test_list = '''
-        RebootStep(id='shutdown'),
-    '''
-    def runTest(self):
-        # Expect a reboot request
-        self.env.shutdown('reboot').AndReturn(True)
-        self.mocker.ReplayAll()
-        self.assertTrue(self.goofy.run_once())
-        self._wait()
+  test_list = '''
+    RebootStep(id='shutdown'),
+  '''
+  def runTest(self):
+    # Expect a reboot request
+    self.env.shutdown('reboot').AndReturn(True)
+    self.mocker.ReplayAll()
+    self.assertTrue(self.goofy.run_once())
+    self._wait()
 
-        # That should have enqueued a task that will cause Goofy
-        # to shut down.
-        self.mocker.ReplayAll()
-        self.assertFalse(self.goofy.run_once())
-        self._wait()
+    # That should have enqueued a task that will cause Goofy
+    # to shut down.
+    self.mocker.ReplayAll()
+    self.assertFalse(self.goofy.run_once())
+    self._wait()
 
-        # Something pretty close to the current time should be written
-        # as the shutdown time.
-        shutdown_time = self.state.get_shared_data('shutdown_time')
-        self.assertTrue(math.fabs(time.time() - shutdown_time) < 2)
+    # Something pretty close to the current time should be written
+    # as the shutdown time.
+    shutdown_time = self.state.get_shared_data('shutdown_time')
+    self.assertTrue(math.fabs(time.time() - shutdown_time) < 2)
 
-        # Fudge the shutdown time to be a long time ago.
-        self.state.set_shared_data(
-            'shutdown_time',
-            time.time() - (factory.Options.max_reboot_time_secs + 1))
+    # Fudge the shutdown time to be a long time ago.
+    self.state.set_shared_data(
+      'shutdown_time',
+      time.time() - (factory.Options.max_reboot_time_secs + 1))
 
-        # Kill and restart Goofy to simulate a reboot.
-        # Goofy should fail the test since it has been too long.
-        self.goofy.destroy()
+    # Kill and restart Goofy to simulate a reboot.
+    # Goofy should fail the test since it has been too long.
+    self.goofy.destroy()
 
-        self.mocker.ResetAll()
-        self.env.create_connection_manager([]).AndReturn(
-            self.connection_manager)
-        self.mocker.ReplayAll()
-        self.goofy = init_goofy(self.env, self.test_list, restart=False)
-        self._wait()
+    self.mocker.ResetAll()
+    self.env.create_connection_manager([]).AndReturn(
+      self.connection_manager)
+    self.mocker.ReplayAll()
+    self.goofy = init_goofy(self.env, self.test_list, restart=False)
+    self._wait()
 
-        state = factory.get_state_instance().get_test_state('shutdown')
-        self.assertEquals(TestState.FAILED, state.status)
-        logging.info('%s', state.error_msg)
-        self.assertTrue(state.error_msg.startswith(
-                'More than %d s elapsed during reboot' %
-                factory.Options.max_reboot_time_secs))
+    test_state = factory.get_state_instance().get_test_state('shutdown')
+    self.assertEquals(TestState.FAILED, test_state.status)
+    logging.info('%s', test_state.error_msg)
+    self.assertTrue(test_state.error_msg.startswith(
+        'More than %d s elapsed during reboot' %
+        factory.Options.max_reboot_time_secs))
 
 
 class NoAutoRunTest(GoofyTest):
-    test_list = ABC_TEST_LIST
-    options = 'options.auto_run_on_start = False'
+  test_list = ABC_TEST_LIST
+  options = 'options.auto_run_on_start = False'
 
-    def _runTestB(self):
-        # There shouldn't be anything to do at startup, since auto_run_on_start
-        # is unset.
-        self.mocker.ReplayAll()
-        self.goofy.run_once()
-        self.assertEqual({}, self.goofy.invocations)
-        self._wait()
+  def _runTestB(self):
+    # There shouldn't be anything to do at startup, since auto_run_on_start
+    # is unset.
+    self.mocker.ReplayAll()
+    self.goofy.run_once()
+    self.assertEqual({}, self.goofy.invocations)
+    self._wait()
 
-        # Tell Goofy to run 'b'.
-        self.check_one_test(
-            'b', 'b_B', True, '',
-            trigger=lambda: self.goofy.handle_switch_test(
-                Event(Event.Type.SWITCH_TEST, path='b')))
+    # Tell Goofy to run 'b'.
+    self.check_one_test(
+      'b', 'b_B', True, '',
+      trigger=lambda: self.goofy.handle_switch_test(
+        Event(Event.Type.SWITCH_TEST, path='b')))
 
-    def runTest(self):
-        self._runTestB()
-        # No more tests to run now.
-        self.mocker.ReplayAll()
-        self.goofy.run_once()
-        self.assertEqual({}, self.goofy.invocations)
+  def runTest(self):
+    self._runTestB()
+    # No more tests to run now.
+    self.mocker.ReplayAll()
+    self.goofy.run_once()
+    self.assertEqual({}, self.goofy.invocations)
 
 
 class AutoRunKeypressTest(NoAutoRunTest):
-    test_list = ABC_TEST_LIST
-    options = '''
-        options.auto_run_on_start = False
-        options.auto_run_on_keypress = True
-    '''
+  test_list = ABC_TEST_LIST
+  options = '''
+    options.auto_run_on_start = False
+    options.auto_run_on_keypress = True
+  '''
 
-    def runTest(self):
-        self._runTestB()
-        # Unlike in NoAutoRunTest, C should now be run.
-        self.check_one_test('c', 'c_C', True, '')
+  def runTest(self):
+    self._runTestB()
+    # Unlike in NoAutoRunTest, C should now be run.
+    self.check_one_test('c', 'c_C', True, '')
 
 
 class PyTestTest(GoofyTest):
-    '''Tests the Python test driver.
+  '''Tests the Python test driver.
 
-    Note that no mocks are used here, since it's easy enough to just have the
-    Python driver run a 'real' test (execpython).
-    '''
-    test_list = '''
-        OperatorTest(id='a', pytest_name='execpython',
-                     dargs={'script': 'assert "Tomato" == "Tomato"'}),
-        OperatorTest(id='b', pytest_name='execpython',
-                     dargs={'script': ("assert 'Pa-TAY-to' == 'Pa-TAH-to', "
-                                       "Let's call the whole thing off")})
-    '''
-    def runTest(self):
-        self.goofy.run_once()
-        self.assertEquals(['a'],
-                          [test.id for test in self.goofy.invocations])
-        self.goofy.wait()
-        self.assertEquals(
-            TestState.PASSED,
-            factory.get_state_instance().get_test_state('a').status)
+  Note that no mocks are used here, since it's easy enough to just have the
+  Python driver run a 'real' test (execpython).
+  '''
+  test_list = '''
+    OperatorTest(id='a', pytest_name='execpython',
+           dargs={'script': 'assert "Tomato" == "Tomato"'}),
+    OperatorTest(id='b', pytest_name='execpython',
+           dargs={'script': ("assert 'Pa-TAY-to' == 'Pa-TAH-to', "
+                     "Let's call the whole thing off")})
+  '''
+  def runTest(self):
+    self.goofy.run_once()
+    self.assertEquals(['a'],
+              [test.id for test in self.goofy.invocations])
+    self.goofy.wait()
+    self.assertEquals(
+      TestState.PASSED,
+      factory.get_state_instance().get_test_state('a').status)
 
-        self.goofy.run_once()
-        self.assertEquals(['b'],
-                          [test.id for test in self.goofy.invocations])
-        self.goofy.wait()
-        failed_state = factory.get_state_instance().get_test_state('b')
-        self.assertEquals(TestState.FAILED, failed_state.status)
-        self.assertTrue(
-            '''Let\'s call the whole thing off''' in failed_state.error_msg,
-            failed_state.error_msg)
+    self.goofy.run_once()
+    self.assertEquals(['b'],
+              [test.id for test in self.goofy.invocations])
+    self.goofy.wait()
+    failed_state = factory.get_state_instance().get_test_state('b')
+    self.assertEquals(TestState.FAILED, failed_state.status)
+    self.assertTrue(
+      '''Let\'s call the whole thing off''' in failed_state.error_msg,
+      failed_state.error_msg)
 
 
 class ConnectionManagerTest(GoofyTest):
-    options = '''
-       options.wlans = [WLAN('foo', 'bar', 'baz')]
-    '''
-    test_list = '''
-       OperatorTest(id='a', autotest_name='a_A'),
-       TestGroup(id='b', exclusive='NETWORKING', subtests=[
-          OperatorTest(id='b1', autotest_name='b_B1'),
-          OperatorTest(id='b2', autotest_name='b_B2'),
-       ]),
-       OperatorTest(id='c', autotest_name='c_C'),
-    '''
-    expected_create_connection_manager_arg = mox.Func(
-        lambda arg: (len(arg) == 1 and
-                     arg[0].__dict__ == dict(ssid='foo',
-                                             security='bar',
-                                             passphrase='baz')))
-    def runTest(self):
-        self.check_one_test('a', 'a_A', True, '')
-        self.connection_manager.DisableNetworking()
-        self.check_one_test('b.b1', 'b_B1', False, 'Uh-oh')
-        self.check_one_test('b.b2', 'b_B2', False, 'Uh-oh')
-        self.connection_manager.EnableNetworking()
-        self.check_one_test('c', 'c_C', True, '')
+  options = '''
+    options.wlans = [WLAN('foo', 'bar', 'baz')]
+  '''
+  test_list = '''
+    OperatorTest(id='a', autotest_name='a_A'),
+    TestGroup(id='b', exclusive='NETWORKING', subtests=[
+      OperatorTest(id='b1', autotest_name='b_B1'),
+      OperatorTest(id='b2', autotest_name='b_B2'),
+    ]),
+    OperatorTest(id='c', autotest_name='c_C'),
+  '''
+  expected_create_connection_manager_arg = mox.Func(
+    lambda arg: (len(arg) == 1 and
+           arg[0].__dict__ == dict(ssid='foo',
+                       security='bar',
+                       passphrase='baz')))
+  def runTest(self):
+    self.check_one_test('a', 'a_A', True, '')
+    self.connection_manager.DisableNetworking()
+    self.check_one_test('b.b1', 'b_B1', False, 'Uh-oh')
+    self.check_one_test('b.b2', 'b_B2', False, 'Uh-oh')
+    self.connection_manager.EnableNetworking()
+    self.check_one_test('c', 'c_C', True, '')
 
 
 if __name__ == "__main__":
-    factory.init_logging('goofy_unittest')
-    goofy._inited_logging = True
-    goofy.suppress_chroot_warning = True
+  factory.init_logging('goofy_unittest')
+  goofy._inited_logging = True
+  goofy.suppress_chroot_warning = True
 
-    unittest.main()
+  unittest.main()
diff --git a/py/goofy/invocation.py b/py/goofy/invocation.py
index 2c28192..3dfc385 100755
--- a/py/goofy/invocation.py
+++ b/py/goofy/invocation.py
@@ -21,7 +21,7 @@
 from optparse import OptionParser
 from StringIO import StringIO
 
-import factory_common
+import factory_common  # pylint: disable=W0611
 from cros.factory.test import factory
 from cros.factory.test.event import Event
 from cros.factory import event_log
@@ -35,438 +35,438 @@
 
 
 class PyTestInfo(object):
-    def __init__(self, test_list, path, pytest_name, args, results_path):
-        self.test_list = test_list
-        self.path = path
-        self.pytest_name = pytest_name
-        self.args = args
-        self.results_path = results_path
+  def __init__(self, test_list, path, pytest_name, args, results_path):
+    self.test_list = test_list
+    self.path = path
+    self.pytest_name = pytest_name
+    self.args = args
+    self.results_path = results_path
 
 
 class TestInvocation(object):
+  '''
+  State for an active test.
+  '''
+  def __init__(self, goofy, test, on_completion=None):
+    '''Constructor.
+
+    @param goofy: The controlling Goofy object.
+    @param test: The FactoryTest object to test.
+    @param on_completion: Callback to invoke in the goofy event queue
+      on completion.
     '''
-    State for an active test.
+    self.goofy = goofy
+    self.test = test
+    self.thread = threading.Thread(target=self._run,
+                     name='TestInvocation-%s' % test.path)
+    self.on_completion = on_completion
+    self.uuid = event_log.TimedUuid()
+    self.env_additions = {'CROS_FACTORY_TEST_PATH': self.test.path,
+                'CROS_FACTORY_TEST_INVOCATION': self.uuid}
+    self.debug_log_path = None
+    self._lock = threading.Lock()
+    # The following properties are guarded by the lock.
+    self._aborted = False
+    self._completed = False
+    self._process = None
+
+  def __repr__(self):
+    return 'TestInvocation(_aborted=%s, _completed=%s)' % (
+      self._aborted, self._completed)
+
+  def start(self):
+    '''Starts the test thread.'''
+    self.thread.start()
+
+  def abort_and_join(self):
     '''
-    def __init__(self, goofy, test, on_completion=None):
-        '''Constructor.
+    Aborts a test (must be called from the event controller thread).
+    '''
+    with self._lock:
+      self._aborted = True
+      if self._process:
+        utils.kill_process_tree(self._process, 'autotest')
+    if self.thread:
+      self.thread.join()
+    with self._lock:
+      # Should be set by the thread itself, but just in case...
+      self._completed = True
 
-        @param goofy: The controlling Goofy object.
-        @param test: The FactoryTest object to test.
-        @param on_completion: Callback to invoke in the goofy event queue
-            on completion.
-        '''
-        self.goofy = goofy
-        self.test = test
-        self.thread = threading.Thread(target=self._run,
-                                       name='TestInvocation-%s' % test.path)
-        self.on_completion = on_completion
-        self.uuid = event_log.TimedUuid()
-        self.env_additions = {'CROS_FACTORY_TEST_PATH': self.test.path,
-                              'CROS_FACTORY_TEST_INVOCATION': self.uuid}
-        self.debug_log_path = None
-        self._lock = threading.Lock()
-        # The following properties are guarded by the lock.
-        self._aborted = False
-        self._completed = False
-        self._process = None
+  def is_completed(self):
+    '''
+    Returns true if the test has finished.
+    '''
+    with self._lock:
+      return self._completed
 
-    def __repr__(self):
-        return 'TestInvocation(_aborted=%s, _completed=%s)' % (
-            self._aborted, self._completed)
+  def _invoke_autotest(self):
+    '''
+    Invokes an autotest test.
 
-    def start(self):
-        '''Starts the test thread.'''
-        self.thread.start()
+    This method encapsulates all the magic necessary to run a single
+    autotest test using the 'autotest' command-line tool and get a
+    sane pass/fail status and error message out.  It may be better
+    to just write our own command-line wrapper for job.run_test
+    instead.
 
-    def abort_and_join(self):
-        '''
-        Aborts a test (must be called from the event controller thread).
-        '''
-        with self._lock:
-            self._aborted = True
-            if self._process:
-                utils.kill_process_tree(self._process, 'autotest')
-        if self.thread:
-            self.thread.join()
-        with self._lock:
-            # Should be set by the thread itself, but just in case...
-            self._completed = True
+    @param test: the autotest to run
+    @param dargs: the argument map
+    @return: tuple of status (TestState.PASSED or TestState.FAILED) and
+      error message, if any
+    '''
+    assert self.test.autotest_name
 
-    def is_completed(self):
-        '''
-        Returns true if the test has finished.
-        '''
-        with self._lock:
-            return self._completed
+    test_tag = '%s_%s' % (self.test.path, self.count)
+    dargs = dict(self.test.dargs)
+    dargs.update({'tag': test_tag,
+            'test_list_path': self.goofy.options.test_list})
 
-    def _invoke_autotest(self):
-        '''
-        Invokes an autotest test.
+    status = TestState.FAILED
+    error_msg = 'Unknown'
 
-        This method encapsulates all the magic necessary to run a single
-        autotest test using the 'autotest' command-line tool and get a
-        sane pass/fail status and error message out.  It may be better
-        to just write our own command-line wrapper for job.run_test
-        instead.
+    try:
+      output_dir = '%s/results/%s-%s' % (factory.CLIENT_PATH,
+                         self.test.path,
+                         self.uuid)
+      self.debug_log_path = os.path.join(
+        output_dir,
+        'results/default/debug/client.INFO')
+      if not os.path.exists(output_dir):
+        os.makedirs(output_dir)
+      tmp_dir = tempfile.mkdtemp(prefix='tmp', dir=output_dir)
 
-        @param test: the autotest to run
-        @param dargs: the argument map
-        @return: tuple of status (TestState.PASSED or TestState.FAILED) and
-            error message, if any
-        '''
-        assert self.test.autotest_name
+      control_file = os.path.join(tmp_dir, 'control')
+      result_file = os.path.join(tmp_dir, 'result')
+      args_file = os.path.join(tmp_dir, 'args')
 
-        test_tag = '%s_%s' % (self.test.path, self.count)
-        dargs = dict(self.test.dargs)
-        dargs.update({'tag': test_tag,
-                      'test_list_path': self.goofy.options.test_list})
+      with open(args_file, 'w') as f:
+        pickle.dump(dargs, f)
 
-        status = TestState.FAILED
-        error_msg = 'Unknown'
+      # Create a new control file to use to run the test
+      with open(control_file, 'w') as f:
+        print >> f, 'import common, traceback, utils'
+        print >> f, 'import cPickle as pickle'
+        print >> f, ("success = job.run_test("
+              "'%s', **pickle.load(open('%s')))" % (
+          self.test.autotest_name, args_file))
 
+        print >> f, (
+          "pickle.dump((success, "
+          "str(job.last_error) if job.last_error else None), "
+          "open('%s', 'w'), protocol=2)"
+          % result_file)
+
+      args = [os.path.join(os.path.dirname(factory.FACTORY_PATH),
+                 'autotest/bin/autotest'),
+          '--output_dir', output_dir,
+          control_file]
+
+      logging.debug('Test command line: %s', ' '.join(
+          [pipes.quote(arg) for arg in args]))
+
+      with self._lock:
+        with self.goofy.env.lock:
+          self._process = self.goofy.env.spawn_autotest(
+            self.test.autotest_name, args, self.env_additions,
+            result_file)
+
+      returncode = self._process.wait()
+      with self._lock:
+        if self._aborted:
+          error_msg = 'Aborted by operator'
+          return
+
+      if returncode:
+        # Only happens when there is an autotest-level problem (not when
+        # the test actually failed).
+        error_msg = 'autotest returned with code %d' % returncode
+        return
+
+      with open(result_file) as f:
         try:
-            output_dir = '%s/results/%s-%s' % (factory.CLIENT_PATH,
-                                               self.test.path,
-                                               self.uuid)
-            self.debug_log_path = os.path.join(
-                output_dir,
-                'results/default/debug/client.INFO')
-            if not os.path.exists(output_dir):
-                os.makedirs(output_dir)
-            tmp_dir = tempfile.mkdtemp(prefix='tmp', dir=output_dir)
-
-            control_file = os.path.join(tmp_dir, 'control')
-            result_file = os.path.join(tmp_dir, 'result')
-            args_file = os.path.join(tmp_dir, 'args')
-
-            with open(args_file, 'w') as f:
-                pickle.dump(dargs, f)
-
-            # Create a new control file to use to run the test
-            with open(control_file, 'w') as f:
-                print >> f, 'import common, traceback, utils'
-                print >> f, 'import cPickle as pickle'
-                print >> f, ("success = job.run_test("
-                            "'%s', **pickle.load(open('%s')))" % (
-                    self.test.autotest_name, args_file))
-
-                print >> f, (
-                    "pickle.dump((success, "
-                    "str(job.last_error) if job.last_error else None), "
-                    "open('%s', 'w'), protocol=2)"
-                    % result_file)
-
-            args = [os.path.join(os.path.dirname(factory.FACTORY_PATH),
-                                 'autotest/bin/autotest'),
-                    '--output_dir', output_dir,
-                    control_file]
-
-            logging.debug('Test command line: %s', ' '.join(
-                    [pipes.quote(arg) for arg in args]))
-
-            with self._lock:
-                with self.goofy.env.lock:
-                    self._process = self.goofy.env.spawn_autotest(
-                        self.test.autotest_name, args, self.env_additions,
-                        result_file)
-
-            returncode = self._process.wait()
-            with self._lock:
-                if self._aborted:
-                    error_msg = 'Aborted by operator'
-                    return
-
-            if returncode:
-                # Only happens when there is an autotest-level problem (not when
-                # the test actually failed).
-                error_msg = 'autotest returned with code %d' % returncode
-                return
-
-            with open(result_file) as f:
-                try:
-                    success, error_msg = pickle.load(f)
-                except:  # pylint: disable=W0702
-                    logging.exception('Unable to retrieve autotest results')
-                    error_msg = 'Unable to retrieve autotest results'
-                    return
-
-            if success:
-                status = TestState.PASSED
-                error_msg = ''
-        except Exception:  # pylint: disable=W0703
-            logging.exception('Exception in autotest driver')
-            # Make sure Goofy reports the exception upon destruction
-            # (e.g., for testing)
-            self.goofy.record_exception(traceback.format_exception_only(
-                    *sys.exc_info()[:2]))
-        finally:
-            self.clean_autotest_logs(output_dir)
-            return status, error_msg  # pylint: disable=W0150
-
-    def _invoke_pytest(self):
-        '''
-        Invokes a pyunittest-based test.
-        '''
-        assert self.test.pytest_name
-
-        files_to_delete = []
-        try:
-            def make_tmp(type):
-                ret = tempfile.mktemp(
-                    prefix='%s-%s-' % (self.test.path, type))
-                files_to_delete.append(ret)
-                return ret
-
-            info_path = make_tmp('info')
-            results_path = make_tmp('results')
-
-            log_dir = os.path.join(factory.get_log_root(),
-                                   'factory_test_logs')
-            if not os.path.exists(log_dir):
-                os.makedirs(log_dir)
-            log_path = os.path.join(log_dir,
-                                    '%s.%03d' % (self.test.path,
-                                                 self.count))
-
-            with open(info_path, 'w') as info:
-                pickle.dump(PyTestInfo(
-                        test_list=self.goofy.options.test_list,
-                        path=self.test.path,
-                        pytest_name=self.test.pytest_name,
-                        args=self.test.dargs,
-                        results_path = results_path),
-                            info)
-
-            # Invoke the unittest driver in a separate process.
-            with open(log_path, "w") as log:
-                this_file = os.path.realpath(__file__)
-                this_file = re.sub(r'\.pyc$', '.py', this_file)
-                args = [this_file, '--pytest', info_path]
-                logging.debug('Test command line: %s >& %s',
-                             ' '.join([pipes.quote(arg) for arg in args]),
-                             log_path)
-
-                env = dict(os.environ)
-                env.update(self.env_additions)
-                with self._lock:
-                    if self._aborted:
-                        return TestState.FAILED, 'Aborted before starting'
-                    self._process = subprocess.Popen(
-                        args,
-                        env=env,
-                        stdin=open(os.devnull, "w"),
-                        stdout=log,
-                        stderr=subprocess.STDOUT)
-                self._process.wait()
-                with self._lock:
-                    if self._aborted:
-                        return TestState.FAILED, 'Aborted by operator'
-                if self._process.returncode:
-                    return TestState.FAILED, (
-                        'Test returned code %d' % pytest.returncode)
-
-            if not os.path.exists(results_path):
-                return TestState.FAILED, 'pytest did not complete'
-
-            with open(results_path) as f:
-                return pickle.load(f)
+          success, error_msg = pickle.load(f)
         except:  # pylint: disable=W0702
-            logging.exception('Unable to retrieve pytest results')
-            return TestState.FAILED, 'Unable to retrieve pytest results'
-        finally:
-            for f in files_to_delete:
-                try:
-                    if os.path.exists(f):
-                        os.unlink(f)
-                except:
-                    logging.exception('Unable to delete temporary file %s',
-                                      f)
+          logging.exception('Unable to retrieve autotest results')
+          error_msg = 'Unable to retrieve autotest results'
+          return
 
-    def _invoke_target(self):
-        '''
-        Invokes a target directly within Goofy.
-        '''
-        try:
-            self.test.invocation_target(self)
-            return TestState.PASSED, ''
-        except:
-            logging.exception('Exception while invoking target')
-            error_msg = traceback.format_exc()
-            return TestState.FAILED, traceback.format_exc()
+      if success:
+        status = TestState.PASSED
+        error_msg = ''
+    except Exception:  # pylint: disable=W0703
+      logging.exception('Exception in autotest driver')
+      # Make sure Goofy reports the exception upon destruction
+      # (e.g., for testing)
+      self.goofy.record_exception(traceback.format_exception_only(
+          *sys.exc_info()[:2]))
+    finally:
+      self.clean_autotest_logs(output_dir)
+      return status, error_msg  # pylint: disable=W0150
 
-    def clean_autotest_logs(self, output_dir):
-        globs = self.goofy.test_list.options.preserve_autotest_results
-        if '*' in globs:
-            # Keep everything
-            return
+  def _invoke_pytest(self):
+    '''
+    Invokes a pyunittest-based test.
+    '''
+    assert self.test.pytest_name
 
-        deleted_count = 0
-        preserved_count = 0
-        for root, dirs, files in os.walk(output_dir, topdown=False):
-            for f in files:
-                if any(fnmatch.fnmatch(f, g)
-                       for g in globs):
-                    # Keep it
-                    preserved_count = 1
-                else:
-                    try:
-                        os.unlink(os.path.join(root, f))
-                        deleted_count += 1
-                    except:
-                        logging.exception('Unable to remove %s' %
-                                          os.path.join(root, f))
-            try:
-                # Try to remove the directory (in case it's empty now)
-                os.rmdir(root)
-            except:
-                # Not empty; that's OK
-                pass
-        logging.info('Preserved %d files matching %s and removed %d',
-                     preserved_count, globs, deleted_count)
+    files_to_delete = []
+    try:
+      def make_tmp(type):
+        ret = tempfile.mktemp(
+          prefix='%s-%s-' % (self.test.path, type))
+        files_to_delete.append(ret)
+        return ret
 
-    def _run(self):
-        with self._lock:
-            if self._aborted:
-                return
+      info_path = make_tmp('info')
+      results_path = make_tmp('results')
 
-        self.count = self.test.update_state(
-            status=TestState.ACTIVE, increment_count=1, error_msg='',
-            invocation=self.uuid).count
+      log_dir = os.path.join(factory.get_log_root(),
+                   'factory_test_logs')
+      if not os.path.exists(log_dir):
+        os.makedirs(log_dir)
+      log_path = os.path.join(log_dir,
+                  '%s.%03d' % (self.test.path,
+                         self.count))
 
-        factory.console.info('Running test %s' % self.test.path)
-
-        log_args = dict(
+      with open(info_path, 'w') as info:
+        pickle.dump(PyTestInfo(
+            test_list=self.goofy.options.test_list,
             path=self.test.path,
-            # Use Python representation for dargs, since some elements
-            # may not be representable in YAML.
-            dargs=repr(self.test.dargs),
-            invocation=self.uuid)
-        if self.test.autotest_name:
-            log_args['autotest_name'] = self.test.autotest_name
-        if self.test.pytest_name:
-            log_args['pytest_name'] = self.test.pytest_name
+            pytest_name=self.test.pytest_name,
+            args=self.test.dargs,
+            results_path = results_path),
+              info)
 
-        self.goofy.event_log.Log('start_test', **log_args)
-        start_time = time.time()
-        try:
-            if self.test.autotest_name:
-                status, error_msg = self._invoke_autotest()
-            elif self.test.pytest_name:
-                status, error_msg = self._invoke_pytest()
-            elif self.test.invocation_target:
-                status, error_msg = self._invoke_target()
-            else:
-                status = TestState.FAILED
-                error_msg = (
-                    'No autotest_name, pytest_name, or invocation_target')
-        finally:
-            try:
-                self.goofy.event_client.post_event(
-                    Event(Event.Type.DESTROY_TEST,
-                          test=self.test.path,
-                          invocation=self.uuid))
-            except:
-                logging.exception('Unable to post END_TEST event')
+      # Invoke the unittest driver in a separate process.
+      with open(log_path, "w") as log:
+        this_file = os.path.realpath(__file__)
+        this_file = re.sub(r'\.pyc$', '.py', this_file)
+        args = [this_file, '--pytest', info_path]
+        logging.debug('Test command line: %s >& %s',
+               ' '.join([pipes.quote(arg) for arg in args]),
+               log_path)
 
-            try:
-                # Leave all items in log_args; this duplicates
-                # things but will make it easier to grok the output.
-                log_args.update(dict(status=status,
-                                     duration=time.time() - start_time))
-                if error_msg:
-                    log_args['error_msg'] = error_msg
-                if (status != TestState.PASSED and
-                    self.debug_log_path and
-                    os.path.exists(self.debug_log_path)):
-                    try:
-                        debug_log_size = os.path.getsize(self.debug_log_path)
-                        offset = max(0, debug_log_size - ERROR_LOG_TAIL_LENGTH)
-                        with open(self.debug_log_path) as f:
-                            f.seek(offset)
-                            log_args['log_tail'] = f.read()
-                    except:
-                        logging.exception('Unable to read log tail')
-                self.goofy.event_log.Log('end_test', **log_args)
-            except:
-                logging.exception('Unable to log end_test event')
-
-        factory.console.info('Test %s %s%s',
-                             self.test.path,
-                             status,
-                             ': %s' % error_msg if error_msg else '')
-
-        self.test.update_state(status=status, error_msg=error_msg,
-                               visible=False)
+        env = dict(os.environ)
+        env.update(self.env_additions)
         with self._lock:
-            self._completed = True
+          if self._aborted:
+            return TestState.FAILED, 'Aborted before starting'
+          self._process = subprocess.Popen(
+            args,
+            env=env,
+            stdin=open(os.devnull, "w"),
+            stdout=log,
+            stderr=subprocess.STDOUT)
+        self._process.wait()
+        with self._lock:
+          if self._aborted:
+            return TestState.FAILED, 'Aborted by operator'
+        if self._process.returncode:
+          return TestState.FAILED, (
+            'Test returned code %d' % pytest.returncode)
 
-        self.goofy.run_queue.put(self.goofy.reap_completed_tests)
-        if self.on_completion:
-            self.goofy.run_queue.put(self.on_completion)
+      if not os.path.exists(results_path):
+        return TestState.FAILED, 'pytest did not complete'
+
+      with open(results_path) as f:
+        return pickle.load(f)
+    except:  # pylint: disable=W0702
+      logging.exception('Unable to retrieve pytest results')
+      return TestState.FAILED, 'Unable to retrieve pytest results'
+    finally:
+      for f in files_to_delete:
+        try:
+          if os.path.exists(f):
+            os.unlink(f)
+        except:
+          logging.exception('Unable to delete temporary file %s',
+                    f)
+
+  def _invoke_target(self):
+    '''
+    Invokes a target directly within Goofy.
+    '''
+    try:
+      self.test.invocation_target(self)
+      return TestState.PASSED, ''
+    except:
+      logging.exception('Exception while invoking target')
+      error_msg = traceback.format_exc()
+      return TestState.FAILED, traceback.format_exc()
+
+  def clean_autotest_logs(self, output_dir):
+    globs = self.goofy.test_list.options.preserve_autotest_results
+    if '*' in globs:
+      # Keep everything
+      return
+
+    deleted_count = 0
+    preserved_count = 0
+    for root, dirs, files in os.walk(output_dir, topdown=False):
+      for f in files:
+        if any(fnmatch.fnmatch(f, g)
+             for g in globs):
+          # Keep it
+          preserved_count = 1
+        else:
+          try:
+            os.unlink(os.path.join(root, f))
+            deleted_count += 1
+          except:
+            logging.exception('Unable to remove %s' %
+                      os.path.join(root, f))
+      try:
+        # Try to remove the directory (in case it's empty now)
+        os.rmdir(root)
+      except:
+        # Not empty; that's OK
+        pass
+    logging.info('Preserved %d files matching %s and removed %d',
+           preserved_count, globs, deleted_count)
+
+  def _run(self):
+    with self._lock:
+      if self._aborted:
+        return
+
+    self.count = self.test.update_state(
+      status=TestState.ACTIVE, increment_count=1, error_msg='',
+      invocation=self.uuid).count
+
+    factory.console.info('Running test %s' % self.test.path)
+
+    log_args = dict(
+      path=self.test.path,
+      # Use Python representation for dargs, since some elements
+      # may not be representable in YAML.
+      dargs=repr(self.test.dargs),
+      invocation=self.uuid)
+    if self.test.autotest_name:
+      log_args['autotest_name'] = self.test.autotest_name
+    if self.test.pytest_name:
+      log_args['pytest_name'] = self.test.pytest_name
+
+    self.goofy.event_log.Log('start_test', **log_args)
+    start_time = time.time()
+    try:
+      if self.test.autotest_name:
+        status, error_msg = self._invoke_autotest()
+      elif self.test.pytest_name:
+        status, error_msg = self._invoke_pytest()
+      elif self.test.invocation_target:
+        status, error_msg = self._invoke_target()
+      else:
+        status = TestState.FAILED
+        error_msg = (
+          'No autotest_name, pytest_name, or invocation_target')
+    finally:
+      try:
+        self.goofy.event_client.post_event(
+          Event(Event.Type.DESTROY_TEST,
+              test=self.test.path,
+              invocation=self.uuid))
+      except:
+        logging.exception('Unable to post END_TEST event')
+
+      try:
+        # Leave all items in log_args; this duplicates
+        # things but will make it easier to grok the output.
+        log_args.update(dict(status=status,
+                   duration=time.time() - start_time))
+        if error_msg:
+          log_args['error_msg'] = error_msg
+        if (status != TestState.PASSED and
+          self.debug_log_path and
+          os.path.exists(self.debug_log_path)):
+          try:
+            debug_log_size = os.path.getsize(self.debug_log_path)
+            offset = max(0, debug_log_size - ERROR_LOG_TAIL_LENGTH)
+            with open(self.debug_log_path) as f:
+              f.seek(offset)
+              log_args['log_tail'] = f.read()
+          except:
+            logging.exception('Unable to read log tail')
+        self.goofy.event_log.Log('end_test', **log_args)
+      except:
+        logging.exception('Unable to log end_test event')
+
+    factory.console.info('Test %s %s%s',
+               self.test.path,
+               status,
+               ': %s' % error_msg if error_msg else '')
+
+    self.test.update_state(status=status, error_msg=error_msg,
+                 visible=False)
+    with self._lock:
+      self._completed = True
+
+    self.goofy.run_queue.put(self.goofy.reap_completed_tests)
+    if self.on_completion:
+      self.goofy.run_queue.put(self.on_completion)
 
 
 def run_pytest(test_info):
-    '''Runs a pytest, saving a pickled (status, error_msg) tuple to the
-    appropriate results file.
+  '''Runs a pytest, saving a pickled (status, error_msg) tuple to the
+  appropriate results file.
 
-    Args:
-        test_info: A PyTestInfo object containing information about what to
-            run.
-    '''
-    try:
-        __import__('cros.factory.test.pytests.%s' % test_info.pytest_name)
-        module = getattr(pytests, test_info.pytest_name)
-        suite = unittest.TestLoader().loadTestsFromModule(module)
+  Args:
+    test_info: A PyTestInfo object containing information about what to
+      run.
+  '''
+  try:
+    __import__('cros.factory.test.pytests.%s' % test_info.pytest_name)
+    module = getattr(pytests, test_info.pytest_name)
+    suite = unittest.TestLoader().loadTestsFromModule(module)
 
-        # Recursively set
-        def set_test_info(test):
-            if isinstance(test, unittest.TestCase):
-                test.test_info = test_info
-            elif isinstance(test, unittest.TestSuite):
-                for x in test:
-                    set_test_info(x)
-        set_test_info(suite)
+    # Recursively set
+    def set_test_info(test):
+      if isinstance(test, unittest.TestCase):
+        test.test_info = test_info
+      elif isinstance(test, unittest.TestSuite):
+        for x in test:
+          set_test_info(x)
+    set_test_info(suite)
 
-        runner = unittest.TextTestRunner()
-        result = runner.run(suite)
+    runner = unittest.TextTestRunner()
+    result = runner.run(suite)
 
-        def format_error_msg(test_name, trace):
-            '''Formats a trace so that the actual error message is in the last
-            line.
-            '''
-            # The actual error is in the last line.
-            trace, _, error_msg = trace.strip().rpartition('\n')
-            error_msg = error_msg.replace('FactoryTestFailure: ', '')
-            return error_msg + '\n' + trace
+    def format_error_msg(test_name, trace):
+      '''Formats a trace so that the actual error message is in the last
+      line.
+      '''
+      # The actual error is in the last line.
+      trace, _, error_msg = trace.strip().rpartition('\n')
+      error_msg = error_msg.replace('FactoryTestFailure: ', '')
+      return error_msg + '\n' + trace
 
-        all_failures = result.failures + result.errors
-        if all_failures:
-            status = TestState.FAILED
-            error_msg = '; '.join(format_error_msg(test_name, trace)
-                                  for test_name, trace in all_failures)
-            logging.info('pytest failure: %s', error_msg)
-        else:
-            status = TestState.PASSED
-            error_msg = ''
-    except:
-        logging.exception('Unable to run pytest')
-        status = TestState.FAILED
-        error_msg = traceback.format_exc()
+    all_failures = result.failures + result.errors
+    if all_failures:
+      status = TestState.FAILED
+      error_msg = '; '.join(format_error_msg(test_name, trace)
+                  for test_name, trace in all_failures)
+      logging.info('pytest failure: %s', error_msg)
+    else:
+      status = TestState.PASSED
+      error_msg = ''
+  except:
+    logging.exception('Unable to run pytest')
+    status = TestState.FAILED
+    error_msg = traceback.format_exc()
 
-    with open(test_info.results_path, 'w') as results:
-        pickle.dump((status, error_msg), results)
+  with open(test_info.results_path, 'w') as results:
+    pickle.dump((status, error_msg), results)
 
 def main():
-    parser = OptionParser()
-    parser.add_option('--pytest', dest='pytest_info',
-                      help='Info for pytest to run')
-    (options, args) = parser.parse_args()
+  parser = OptionParser()
+  parser.add_option('--pytest', dest='pytest_info',
+            help='Info for pytest to run')
+  (options, args) = parser.parse_args()
 
-    assert options.pytest_info
+  assert options.pytest_info
 
-    info = pickle.load(open(options.pytest_info))
-    factory.init_logging(info.path)
-    run_pytest(info)
+  info = pickle.load(open(options.pytest_info))
+  factory.init_logging(info.path)
+  run_pytest(info)
 
 if __name__ == '__main__':
-    main()
+  main()
diff --git a/py/goofy/prespawner.py b/py/goofy/prespawner.py
index 405c2cb..7ef513a 100644
--- a/py/goofy/prespawner.py
+++ b/py/goofy/prespawner.py
@@ -19,67 +19,67 @@
 
 
 class Prespawner():
-    def __init__(self):
-        self.prespawned = Queue(NUM_PRESPAWNED_PROCESSES)
-        self.thread = None
-        self.terminated = False
+  def __init__(self):
+    self.prespawned = Queue(NUM_PRESPAWNED_PROCESSES)
+    self.thread = None
+    self.terminated = False
 
-    def spawn(self, args, env_additions=None):
-        '''
-        Spawns a new autotest (reusing an prespawned process if available).
+  def spawn(self, args, env_additions=None):
+    '''
+    Spawns a new autotest (reusing an prespawned process if available).
 
-        @param args: A list of arguments (sys.argv)
-        @param env_additions: Items to add to the current environment
-        '''
-        new_env = dict(os.environ)
-        if env_additions:
-            new_env.update(env_additions)
+    @param args: A list of arguments (sys.argv)
+    @param env_additions: Items to add to the current environment
+    '''
+    new_env = dict(os.environ)
+    if env_additions:
+      new_env.update(env_additions)
 
-        process = self.prespawned.get()
-        # Write the environment and argv to the process's stdin; it will launch
-        # autotest once these are received.
-        pickle.dump((new_env, args), process.stdin, protocol=2)
-        process.stdin.close()
-        return process
+    process = self.prespawned.get()
+    # Write the environment and argv to the process's stdin; it will launch
+    # autotest once these are received.
+    pickle.dump((new_env, args), process.stdin, protocol=2)
+    process.stdin.close()
+    return process
 
-    def start(self):
-        '''
-        Starts a thread to pre-spawn autotests.
-        '''
-        def run():
-            while not self.terminated:
-                process = subprocess.Popen(
-                    ['python', '-u', PRESPAWNER_PATH,
-                     '--prespawn_autotest'],
-                    cwd=os.path.dirname(PRESPAWNER_PATH),
-                    stdin=subprocess.PIPE)
-                logging.debug('Pre-spawned an autotest process %d', process.pid)
-                self.prespawned.put(process)
+  def start(self):
+    '''
+    Starts a thread to pre-spawn autotests.
+    '''
+    def run():
+      while not self.terminated:
+        process = subprocess.Popen(
+          ['python', '-u', PRESPAWNER_PATH,
+           '--prespawn_autotest'],
+          cwd=os.path.dirname(PRESPAWNER_PATH),
+          stdin=subprocess.PIPE)
+        logging.debug('Pre-spawned an autotest process %d', process.pid)
+        self.prespawned.put(process)
 
-            # Let stop() know that we are done
-            self.prespawned.put(None)
+      # Let stop() know that we are done
+      self.prespawned.put(None)
 
-        if not self.thread and os.path.exists(PRESPAWNER_PATH):
-            self.thread = threading.Thread(target=run, name='Prespawner')
-            self.thread.start()
+    if not self.thread and os.path.exists(PRESPAWNER_PATH):
+      self.thread = threading.Thread(target=run, name='Prespawner')
+      self.thread.start()
 
-    def stop(self):
-        '''
-        Stops the pre-spawn thread gracefully.
-        '''
-        if not self.thread:
-            # Never started
-            return
+  def stop(self):
+    '''
+    Stops the pre-spawn thread gracefully.
+    '''
+    if not self.thread:
+      # Never started
+      return
 
-        self.terminated = True
-        # Wait for any existing prespawned processes.
-        while True:
-            process = self.prespawned.get()
-            if not process:
-                break
-            # Send a 'None' environment and arg list to tell the prespawner
-            # processes to exit.
-            pickle.dump((None, None), process.stdin, protocol=2)
-            process.stdin.close()
-            process.wait()
-        self.thread = None
+    self.terminated = True
+    # Wait for any existing prespawned processes.
+    while True:
+      process = self.prespawned.get()
+      if not process:
+        break
+      # Send a 'None' environment and arg list to tell the prespawner
+      # processes to exit.
+      pickle.dump((None, None), process.stdin, protocol=2)
+      process.stdin.close()
+      process.wait()
+    self.thread = None
diff --git a/py/goofy/system.py b/py/goofy/system.py
index 7155b03..f36f7bd 100644
--- a/py/goofy/system.py
+++ b/py/goofy/system.py
@@ -10,116 +10,116 @@
 import time
 import yaml
 
-import factory_common
+import factory_common  # pylint: disable=W0611
 from cros.factory.test import factory
 from cros.factory.test import shopfloor
 
 
 class SystemInfo(object):
-    '''Static information about the system.
+  '''Static information about the system.
 
-    This is mostly static information that changes rarely if ever
-    (e.g., version numbers, serial numbers, etc.).
-    '''
-    def __init__(self):
-        self.serial_number = None
-        try:
-            self.serial_number = shopfloor.get_serial_number()
-        except:
-            pass
+  This is mostly static information that changes rarely if ever
+  (e.g., version numbers, serial numbers, etc.).
+  '''
+  def __init__(self):
+    self.serial_number = None
+    try:
+      self.serial_number = shopfloor.get_serial_number()
+    except:
+      pass
 
-        self.factory_image_version = None
-        try:
-            lsb_release = open('/etc/lsb-release').read()
-            match = re.search('^GOOGLE_RELEASE=(.+)$', lsb_release,
-                              re.MULTILINE)
-            if match:
-                self.factory_image_version = match.group(1)
-        except:
-            pass
+    self.factory_image_version = None
+    try:
+      lsb_release = open('/etc/lsb-release').read()
+      match = re.search('^GOOGLE_RELEASE=(.+)$', lsb_release,
+                re.MULTILINE)
+      if match:
+        self.factory_image_version = match.group(1)
+    except:
+      pass
 
-        try:
-            self.wlan0_mac = open('/sys/class/net/wlan0/address').read().strip()
-        except:
-            self.wlan0_mac = None
+    try:
+      self.wlan0_mac = open('/sys/class/net/wlan0/address').read().strip()
+    except:
+      self.wlan0_mac = None
 
-        try:
-            uname = subprocess.Popen(['uname', '-r'], stdout=subprocess.PIPE)
-            stdout, _ = uname.communicate()
-            self.kernel_version = stdout.strip()
-        except:
-            self.kernel_version = None
+    try:
+      uname = subprocess.Popen(['uname', '-r'], stdout=subprocess.PIPE)
+      stdout, _ = uname.communicate()
+      self.kernel_version = stdout.strip()
+    except:
+      self.kernel_version = None
 
-        self.ec_version = None
-        try:
-            ectool = subprocess.Popen(['mosys', 'ec', 'info', '-l'],
-                                      stdout=subprocess.PIPE)
-            stdout, _ = ectool.communicate()
-            match = re.search('^fw_version\s+\|\s+(.+)$', stdout,
-                              re.MULTILINE)
-            if match:
-                self.ec_version = match.group(1)
-        except:
-            pass
+    self.ec_version = None
+    try:
+      ectool = subprocess.Popen(['mosys', 'ec', 'info', '-l'],
+                    stdout=subprocess.PIPE)
+      stdout, _ = ectool.communicate()
+      match = re.search('^fw_version\s+\|\s+(.+)$', stdout,
+                re.MULTILINE)
+      if match:
+        self.ec_version = match.group(1)
+    except:
+      pass
 
-        self.firmware_version = None
-        try:
-            crossystem = subprocess.Popen(['crossystem', 'fwid'],
-                                          stdout=subprocess.PIPE)
-            stdout, _ = crossystem.communicate()
-            self.firmware_version = stdout.strip() or None
-        except:
-            pass
+    self.firmware_version = None
+    try:
+      crossystem = subprocess.Popen(['crossystem', 'fwid'],
+                      stdout=subprocess.PIPE)
+      stdout, _ = crossystem.communicate()
+      self.firmware_version = stdout.strip() or None
+    except:
+      pass
 
-        self.root_device = None
-        try:
-            rootdev = subprocess.Popen(['rootdev', '-s'],
-                                       stdout=subprocess.PIPE)
-            stdout, _ = rootdev.communicate()
-            self.root_device = stdout.strip()
-        except:
-            pass
+    self.root_device = None
+    try:
+      rootdev = subprocess.Popen(['rootdev', '-s'],
+                     stdout=subprocess.PIPE)
+      stdout, _ = rootdev.communicate()
+      self.root_device = stdout.strip()
+    except:
+      pass
 
-        self.factory_md5sum = factory.get_current_md5sum()
+    self.factory_md5sum = factory.get_current_md5sum()
 
 
 class SystemStatus(object):
-    '''Information about the current system status.
+  '''Information about the current system status.
 
-    This is information that changes frequently, e.g., load average
-    or battery information.
-    '''
-    def __init__(self):
-        self.battery = {}
-        for k, item_type in [('charge_full', int),
-                             ('charge_full_design', int),
-                             ('charge_now', int),
-                             ('current_now', int),
-                             ('present', bool),
-                             ('status', str),
-                             ('voltage_min_design', int),
-                             ('voltage_now', int)]:
-            try:
-                self.battery[k] = item_type(
-                    open('/sys/class/power_supply/BAT0/%s' % k).read().strip())
-            except:
-                self.battery[k] = None
+  This is information that changes frequently, e.g., load average
+  or battery information.
+  '''
+  def __init__(self):
+    self.battery = {}
+    for k, item_type in [('charge_full', int),
+               ('charge_full_design', int),
+               ('charge_now', int),
+               ('current_now', int),
+               ('present', bool),
+               ('status', str),
+               ('voltage_min_design', int),
+               ('voltage_now', int)]:
+      try:
+        self.battery[k] = item_type(
+          open('/sys/class/power_supply/BAT0/%s' % k).read().strip())
+      except:
+        self.battery[k] = None
 
-        try:
-            self.load_avg = map(
-                float, open('/proc/loadavg').read().split()[0:3])
-        except:
-            self.load_avg = None
+    try:
+      self.load_avg = map(
+        float, open('/proc/loadavg').read().split()[0:3])
+    except:
+      self.load_avg = None
 
-        try:
-            self.cpu = map(int, open('/proc/stat').readline().split()[1:])
-        except:
-            self.cpu = None
+    try:
+      self.cpu = map(int, open('/proc/stat').readline().split()[1:])
+    except:
+      self.cpu = None
 
 
 if __name__ == '__main__':
-    import yaml
-    print yaml.dump(dict(system_info=SystemInfo(None, None).__dict__,
-                         system_status=SystemStatus().__dict__),
-                    default_flow_style=False)
+  import yaml
+  print yaml.dump(dict(system_info=SystemInfo(None, None).__dict__,
+             system_status=SystemStatus().__dict__),
+          default_flow_style=False)
 
diff --git a/py/goofy/system_unittest.py b/py/goofy/system_unittest.py
index 3aac89f..dcd0d3f 100755
--- a/py/goofy/system_unittest.py
+++ b/py/goofy/system_unittest.py
@@ -6,20 +6,20 @@
 
 import unittest
 
-import factory_common
+import factory_common  # pylint: disable=W0611
 from cros.factory.goofy.system import SystemStatus
 
 
 class SystemStatusTest(unittest.TestCase):
-    def runTest(self):
-        # Don't care about the values; just make sure there's something
-        # there.
-        status = SystemStatus()
-        # Don't check battery, since this system might not even have one.
-        self.assertTrue(isinstance(status.battery, dict))
-        self.assertEquals(3, len(status.load_avg))
-        self.assertEquals(10, len(status.cpu))
+  def runTest(self):
+    # Don't care about the values; just make sure there's something
+    # there.
+    status = SystemStatus()
+    # Don't check battery, since this system might not even have one.
+    self.assertTrue(isinstance(status.battery, dict))
+    self.assertEquals(3, len(status.load_avg))
+    self.assertEquals(10, len(status.cpu))
 
 
 if __name__ == "__main__":
-    unittest.main()
+  unittest.main()
diff --git a/py/goofy/test_environment.py b/py/goofy/test_environment.py
index 515ec8c..fc6589b 100644
--- a/py/goofy/test_environment.py
+++ b/py/goofy/test_environment.py
@@ -13,7 +13,7 @@
 import threading
 import time
 
-import factory_common
+import factory_common  # pylint: disable=W0611
 from cros.factory.test import factory
 from cros.factory.goofy import connection_manager
 from cros.factory.test import state
@@ -21,139 +21,139 @@
 
 
 class Environment(object):
+  '''
+  Abstract base class for external test operations, e.g., run an autotest,
+  shutdown, or reboot.
+
+  The Environment is assumed not to be thread-safe: callers must grab the lock
+  before calling any methods.  This is primarily necessary because we mock out
+  this Environment with mox, and unfortunately mox is not thread-safe.
+  TODO(jsalz): Try to write a thread-safe wrapper for mox.
+  '''
+  lock = threading.Lock()
+
+  def shutdown(self, operation):
     '''
-    Abstract base class for external test operations, e.g., run an autotest,
-    shutdown, or reboot.
+    Shuts the machine down (from a ShutdownStep).
 
-    The Environment is assumed not to be thread-safe: callers must grab the lock
-    before calling any methods.  This is primarily necessary because we mock out
-    this Environment with mox, and unfortunately mox is not thread-safe.
-    TODO(jsalz): Try to write a thread-safe wrapper for mox.
+    Args:
+      operation: 'reboot' or 'halt'.
+
+    Returns:
+      True if Goofy should gracefully exit, or False if Goofy
+        should just consider the shutdown to have suceeded (e.g.,
+        in the chroot).
     '''
-    lock = threading.Lock()
+    raise NotImplementedError()
 
-    def shutdown(self, operation):
-        '''
-        Shuts the machine down (from a ShutdownStep).
+  def launch_chrome(self):
+    '''
+    Launches Chrome.
 
-        Args:
-            operation: 'reboot' or 'halt'.
+    Returns:
+      The Chrome subprocess (or None if none).
+    '''
+    raise NotImplementedError()
 
-        Returns:
-            True if Goofy should gracefully exit, or False if Goofy
-                should just consider the shutdown to have suceeded (e.g.,
-                in the chroot).
-        '''
-        raise NotImplementedError()
+  def spawn_autotest(self, name, args, env_additions, result_file):
+    '''
+    Spawns a process to run an autotest.
 
-    def launch_chrome(self):
-        '''
-        Launches Chrome.
+    Args:
+      name: Name of the autotest to spawn.
+      args: Command-line arguments.
+      env_additions: Additions to the environment.
+      result_file: Expected location of the result file.
+    '''
+    raise NotImplementedError()
 
-        Returns:
-            The Chrome subprocess (or None if none).
-        '''
-        raise NotImplementedError()
-
-    def spawn_autotest(self, name, args, env_additions, result_file):
-        '''
-        Spawns a process to run an autotest.
-
-        Args:
-            name: Name of the autotest to spawn.
-            args: Command-line arguments.
-            env_additions: Additions to the environment.
-            result_file: Expected location of the result file.
-        '''
-        raise NotImplementedError()
-
-    def create_connection_manager(self, wlans):
-        '''
-        Creates a ConnectionManager.
-        '''
-        raise NotImplementedError()
+  def create_connection_manager(self, wlans):
+    '''
+    Creates a ConnectionManager.
+    '''
+    raise NotImplementedError()
 
 
 class DUTEnvironment(Environment):
-    '''
-    A real environment on a device under test.
-    '''
-    def shutdown(self, operation):
-        assert operation in ['reboot', 'halt']
-        logging.info('Shutting down: %s', operation)
-        subprocess.check_call('sync')
-        subprocess.check_call(operation)
-        time.sleep(30)
-        assert False, 'Never reached (should %s)' % operation
+  '''
+  A real environment on a device under test.
+  '''
+  def shutdown(self, operation):
+    assert operation in ['reboot', 'halt']
+    logging.info('Shutting down: %s', operation)
+    subprocess.check_call('sync')
+    subprocess.check_call(operation)
+    time.sleep(30)
+    assert False, 'Never reached (should %s)' % operation
 
-    def spawn_autotest(self, name, args, env_additions, result_file):
-        return self.goofy.prespawner.spawn(args, env_additions)
+  def spawn_autotest(self, name, args, env_additions, result_file):
+    return self.goofy.prespawner.spawn(args, env_additions)
 
-    def launch_chrome(self):
-        # The cursor speed needs to be adjusted when running in QEMU
-        # (but after Chrome starts and has fiddled with the settings
-        # itself).
-        if utils.in_qemu():
-            def FixCursor():
-                for _ in xrange(6):  # Every 500ms for 3 seconds
-                    time.sleep(.5)
-                    subprocess.check_call(['xset','m','200','200'])
+  def launch_chrome(self):
+    # The cursor speed needs to be adjusted when running in QEMU
+    # (but after Chrome starts and has fiddled with the settings
+    # itself).
+    if utils.in_qemu():
+      def FixCursor():
+        for _ in xrange(6):  # Every 500ms for 3 seconds
+          time.sleep(.5)
+          subprocess.check_call(['xset','m','200','200'])
 
-            thread = threading.Thread(target=FixCursor)
-            thread.daemon = True
-            thread.start()
+      thread = threading.Thread(target=FixCursor)
+      thread.daemon = True
+      thread.start()
 
-        chrome_command = [
-            '/opt/google/chrome/chrome',
-            '--user-data-dir=%s/factory-chrome-datadir' %
-            factory.get_log_root(),
-            '--disable-translate',
-            '--aura-host-window-use-fullscreen',
-            '--kiosk',
-            ('--default-device-scale-factor=%d' %
-             self.goofy.options.ui_scale_factor),
-            'http://localhost:%d/' % state.DEFAULT_FACTORY_STATE_PORT,
-            ]
+    chrome_command = [
+      '/opt/google/chrome/chrome',
+      '--user-data-dir=%s/factory-chrome-datadir' %
+      factory.get_log_root(),
+      '--disable-translate',
+      '--aura-host-window-use-fullscreen',
+      '--kiosk',
+      ('--default-device-scale-factor=%d' %
+       self.goofy.options.ui_scale_factor),
+      'http://localhost:%d/' % state.DEFAULT_FACTORY_STATE_PORT,
+      ]
 
-        chrome_log = os.path.join(factory.get_log_root(), 'factory.chrome.log')
-        chrome_log_file = open(chrome_log, "a")
-        logging.info('Launching Chrome; logs in %s' % chrome_log)
-        return subprocess.Popen(chrome_command,
-                                stdout=chrome_log_file,
-                                stderr=subprocess.STDOUT)
+    chrome_log = os.path.join(factory.get_log_root(), 'factory.chrome.log')
+    chrome_log_file = open(chrome_log, "a")
+    logging.info('Launching Chrome; logs in %s' % chrome_log)
+    return subprocess.Popen(chrome_command,
+                stdout=chrome_log_file,
+                stderr=subprocess.STDOUT)
 
-    def create_connection_manager(self, wlans):
-        return connection_manager.ConnectionManager()
+  def create_connection_manager(self, wlans):
+    return connection_manager.ConnectionManager()
 
 
 class FakeChrootEnvironment(Environment):
-    '''
-    A chroot environment that doesn't actually shutdown or run autotests.
-    '''
-    def shutdown(self, operation):
-        assert operation in ['reboot', 'halt']
-        logging.warn('In chroot: skipping %s', operation)
-        return False
+  '''
+  A chroot environment that doesn't actually shutdown or run autotests.
+  '''
+  def shutdown(self, operation):
+    assert operation in ['reboot', 'halt']
+    logging.warn('In chroot: skipping %s', operation)
+    return False
 
-    def spawn_autotest(self, name, args, env_additions, result_file):
-        logging.warn('In chroot: skipping autotest %s', name)
-        # Mark it as passed with 75% probability, or failed with 25%
-        # probability (depending on a hash of the autotest name).
-        pseudo_random = ord(hashlib.sha1(name).digest()[0]) / 256.0
-        passed = pseudo_random > .25
+  def spawn_autotest(self, name, args, env_additions, result_file):
+    logging.warn('In chroot: skipping autotest %s', name)
+    # Mark it as passed with 75% probability, or failed with 25%
+    # probability (depending on a hash of the autotest name).
+    pseudo_random = ord(hashlib.sha1(name).digest()[0]) / 256.0
+    passed = pseudo_random > .25
 
-        with open(result_file, 'w') as out:
-            pickle.dump((passed, '' if passed else 'Simulated failure'), out)
-        # Start a process that will return with a true exit status in
-        # 2 seconds (just like a happy autotest).
-        return subprocess.Popen(['sleep', '2'])
+    with open(result_file, 'w') as out:
+      pickle.dump((passed, '' if passed else 'Simulated failure'), out)
+    # Start a process that will return with a true exit status in
+    # 2 seconds (just like a happy autotest).
+    return subprocess.Popen(['sleep', '2'])
 
-    def launch_chrome(self):
-        logging.warn('In chroot; not launching Chrome. '
-                     'Please open http://localhost:%d/ in Chrome.',
-                     state.DEFAULT_FACTORY_STATE_PORT)
+  def launch_chrome(self):
+    logging.warn('In chroot; not launching Chrome. '
+           'Please open http://localhost:%d/ in Chrome.',
+           state.DEFAULT_FACTORY_STATE_PORT)
 
-    def create_connection_manager(self, wlans):
-        return connection_manager.DummyConnectionManager()
+  def create_connection_manager(self, wlans):
+    return connection_manager.DummyConnectionManager()
 
 
diff --git a/py/goofy/test_steps.py b/py/goofy/test_steps.py
index 13fe995..9f0e6ef 100644
--- a/py/goofy/test_steps.py
+++ b/py/goofy/test_steps.py
@@ -15,16 +15,16 @@
 
 
 class FlushEventLogsStep(FactoryTest):
-    '''Synchronizes event logs.'''
-    def __init__(self, **kw):
-        super(FlushEventLogsStep, self).__init__(invocation_target=self._Run,
-                                                 _default_id='FlushEventLogs')
+  '''Synchronizes event logs.'''
+  def __init__(self, **kw):
+    super(FlushEventLogsStep, self).__init__(invocation_target=self._Run,
+                         _default_id='FlushEventLogs')
 
-    def _Run(self, invocation):
-        log_watcher = invocation.goofy.log_watcher
-        # Display a message on the console if we're going to need to wait
-        if log_watcher.IsScanning():
-            factory.console.info('Waiting for current scan to finish...')
-        factory.console.info('Flushing event logs...')
-        log_watcher.FlushEventLogs()
-        factory.console.info('Flushed event logs.')
+  def _Run(self, invocation):
+    log_watcher = invocation.goofy.log_watcher
+    # Display a message on the console if we're going to need to wait
+    if log_watcher.IsScanning():
+      factory.console.info('Waiting for current scan to finish...')
+    factory.console.info('Flushing event logs...')
+    log_watcher.FlushEventLogs()
+    factory.console.info('Flushed event logs.')
diff --git a/py/goofy/time_sanitizer.py b/py/goofy/time_sanitizer.py
index 44d90ef..0396256 100644
--- a/py/goofy/time_sanitizer.py
+++ b/py/goofy/time_sanitizer.py
@@ -7,14 +7,12 @@
 import argparse
 import ctypes
 import daemon
-import lockfile
 import logging
 import math
-import optparse
 import os
 import time
 
-import factory_common
+import factory_common  # pylint: disable=W0611
 from cros.factory.test import factory
 
 
@@ -91,14 +89,14 @@
       os.makedirs(os.path.dirname(self.state_file))
 
   def Run(self):
-      '''Runs forever, immediately and then every monitor_interval_secs.'''
-      while True:
-        try:
-          self.RunOnce()
-        except:
-          logging.exception()
+    '''Runs forever, immediately and then every monitor_interval_secs.'''
+    while True:
+      try:
+        self.RunOnce()
+      except:  # pylint: disable=W0702
+        logging.exception('Exception in run loop')
 
-        time.sleep(self.monitor_interval_secs)
+      time.sleep(self.monitor_interval_secs)
 
   def RunOnce(self):
     '''Runs once, returning immediately.'''
@@ -107,7 +105,7 @@
       try:
         minimum_time = max(minimum_time,
                            float(open(self.state_file).read().strip()))
-      except:
+      except:  # pylint: disable=W0702
         logging.exception('Unable to read %s', self.state_file)
     else:
       logging.warn('State file %s does not exist', self.state_file)
@@ -137,7 +135,7 @@
     with open(self.state_file, 'w') as f:
       logging.debug('Recording current time %s into %s',
                     _FormatTime(now), self.state_file)
-      print >>f, now
+      print >> f, now
 
 
 def _GetBaseTime(base_time_file):
@@ -150,7 +148,7 @@
       logging.info('Using %s (mtime of %s) as base time',
                    _FormatTime(base_time), base_time_file)
       return base_time
-    except:
+    except:  # pylint: disable=W0702
       logging.exception('Unable to stat %s', base_time_file)
   else:
     logging.warn('base-time-file %s does not exist',
@@ -166,7 +164,7 @@
                       help='file to maintain state across reboots')
   parser.add_argument('--daemon', action='store_true',
                       help=('run as a daemon (to keep known-good time '
-                            'in state file up to date)')),
+                            'in state file up to date)'))
   parser.add_argument('--log', metavar='FILE',
                       default=os.path.join(factory.get_log_root(),
                                            'time_sanitizer.log'),
@@ -177,11 +175,11 @@
   parser.add_argument('--time-bump-secs', metavar='SECS', type=int,
                       default=60,
                       help=('how far ahead to move the time '
-                            'if the clock is hosed')),
+                            'if the clock is hosed'))
   parser.add_argument('--max-leap-secs', metavar='SECS', type=int,
                       default=(SECONDS_PER_DAY * 30),
                       help=('maximum possible time leap without the clock '
-                            'being considered hosed')),
+                            'being considered hosed'))
   parser.add_argument('--verbose', '-v', action='store_true',
                       help='verbose log')
   parser.add_argument('--base-time-file', metavar='FILE',
@@ -210,4 +208,4 @@
 
 
 if __name__ == '__main__':
-    main()
+  main()
diff --git a/py/goofy/time_sanitizer_unittest.py b/py/goofy/time_sanitizer_unittest.py
index 10bc374..ee6419b 100755
--- a/py/goofy/time_sanitizer_unittest.py
+++ b/py/goofy/time_sanitizer_unittest.py
@@ -13,7 +13,7 @@
 
 from contextlib import contextmanager
 
-import factory_common
+import factory_common  # pylint: disable=W0611
 from cros.factory.goofy import time_sanitizer
 
 
@@ -22,8 +22,11 @@
 
 SECONDS_PER_DAY = 86400
 
+
 class TimeSanitizerTest(unittest.TestCase):
   def testBaseTimeFile(self):
+    # pylint: disable=W0212
+    # (access to protected members)
     with tempfile.NamedTemporaryFile() as f:
       self.assertEquals(os.stat(f.name).st_mtime,
                         time_sanitizer._GetBaseTime(f.name))
@@ -84,7 +87,7 @@
       self.fake_time.SetTime(BASE_TIME + 261.5)
     self.assertEquals(BASE_TIME + 261.5, self._ReadStateFile())
 
-if __name__ == "__main__":
-    unittest.main()
 
+if __name__ == "__main__":
+  unittest.main()
 
diff --git a/py/goofy/updater.py b/py/goofy/updater.py
index 7764a75..d653506 100644
--- a/py/goofy/updater.py
+++ b/py/goofy/updater.py
@@ -14,114 +14,114 @@
 
 
 class UpdaterException(Exception):
-    pass
+  pass
 
 
 def CheckCriticalFiles(new_path):
-    '''Raises an exception if certain critical files are missing.'''
-    critical_files = [
-        os.path.join(new_path, f)
-        for f in ['factory/MD5SUM',
-                  'factory/py_pkg/cros/factory/goofy/goofy.py',
-                  'autotest/site_tests/factory_Finalize/factory_Finalize.py']]
-    missing_files = [f for f in critical_files
-                     if not os.path.exists(f)]
-    if missing_files:
-        raise UpdaterException(
-            'Aborting update: Missing critical files %r' % missing_files)
+  '''Raises an exception if certain critical files are missing.'''
+  critical_files = [
+    os.path.join(new_path, f)
+    for f in ['factory/MD5SUM',
+          'factory/py_pkg/cros/factory/goofy/goofy.py',
+          'autotest/site_tests/factory_Finalize/factory_Finalize.py']]
+  missing_files = [f for f in critical_files
+           if not os.path.exists(f)]
+  if missing_files:
+    raise UpdaterException(
+      'Aborting update: Missing critical files %r' % missing_files)
 
 
 def RunRsync(*rsync_command):
-    '''Runs rsync with the given command.'''
-    factory.console.info('Running `%s`',
-                         ' '.join(rsync_command))
-    # Run rsync.
-    rsync = subprocess.Popen(rsync_command,
-                             stdout=subprocess.PIPE,
-                             stderr=subprocess.STDOUT)
-    stdout, _ = rsync.communicate()
-    if stdout:
-        factory.console.info('rsync output: %s', stdout)
-    if rsync.returncode:
-        raise UpdaterException('rsync returned status %d; aborting' %
-                               rsync.returncode)
-    factory.console.info('rsync succeeded')
+  '''Runs rsync with the given command.'''
+  factory.console.info('Running `%s`',
+             ' '.join(rsync_command))
+  # Run rsync.
+  rsync = subprocess.Popen(rsync_command,
+               stdout=subprocess.PIPE,
+               stderr=subprocess.STDOUT)
+  stdout, _ = rsync.communicate()
+  if stdout:
+    factory.console.info('rsync output: %s', stdout)
+  if rsync.returncode:
+    raise UpdaterException('rsync returned status %d; aborting' %
+                 rsync.returncode)
+  factory.console.info('rsync succeeded')
 
 
 def TryUpdate(pre_update_hook=None):
-    '''Attempts to update the autotest directory on the device.
+  '''Attempts to update the autotest directory on the device.
 
-    Atomically replaces the autotest directory with new contents.
-    This routine will always fail in the chroot (to avoid destroying
-    the user's working directory).
+  Atomically replaces the autotest directory with new contents.
+  This routine will always fail in the chroot (to avoid destroying
+  the user's working directory).
 
-    Args:
-        pre_update_hook: A routine to be invoked before the
-            autotest directory is swapped out.
+  Args:
+    pre_update_hook: A routine to be invoked before the
+      autotest directory is swapped out.
 
-    Returns:
-        True if an update was performed and the machine should be
-        rebooted.
-    '''
-    # On a real device, this will resolve to 'autotest' (since 'client'
-    # is a symlink to that).  In the chroot, this will resolve to the
-    # 'client' directory.
-    # Determine whether an update is necessary.
-    current_md5sum = factory.get_current_md5sum()
+  Returns:
+    True if an update was performed and the machine should be
+    rebooted.
+  '''
+  # On a real device, this will resolve to 'autotest' (since 'client'
+  # is a symlink to that).  In the chroot, this will resolve to the
+  # 'client' directory.
+  # Determine whether an update is necessary.
+  current_md5sum = factory.get_current_md5sum()
 
-    url = shopfloor.get_server_url() or shopfloor.detect_default_server_url()
-    factory.console.info(
-        'Checking for updates at <%s>... (current MD5SUM is %s)',
-        url, current_md5sum)
+  url = shopfloor.get_server_url() or shopfloor.detect_default_server_url()
+  factory.console.info(
+    'Checking for updates at <%s>... (current MD5SUM is %s)',
+    url, current_md5sum)
 
-    shopfloor_client = shopfloor.get_instance(detect=True)
-    new_md5sum = shopfloor_client.GetTestMd5sum()
-    factory.console.info('MD5SUM from server is %s', new_md5sum)
-    if current_md5sum == new_md5sum or new_md5sum is None:
-        factory.console.info('Factory software is up to date')
-        return False
+  shopfloor_client = shopfloor.get_instance(detect=True)
+  new_md5sum = shopfloor_client.GetTestMd5sum()
+  factory.console.info('MD5SUM from server is %s', new_md5sum)
+  if current_md5sum == new_md5sum or new_md5sum is None:
+    factory.console.info('Factory software is up to date')
+    return False
 
-    # /usr/local on the device (parent to both factory and autotest)
-    parent_dir = os.path.dirname(factory.FACTORY_PATH)
+  # /usr/local on the device (parent to both factory and autotest)
+  parent_dir = os.path.dirname(factory.FACTORY_PATH)
 
-    # An update is necessary.  Construct the rsync command.
-    update_port = shopfloor_client.GetUpdatePort()
-    new_path = os.path.join(parent_dir, 'updater.new')
-    RunRsync(
-        'rsync',
-        '-a', '--delete', '--stats',
-        # Use copies of identical files from the old autotest
-        # as much as possible to save network bandwidth.
-        '--copy-dest=%s' % parent_dir,
-        'rsync://%s:%d/factory/%s/' % (
-            urlparse(url).hostname,
-            update_port,
-            new_md5sum),
-        '%s/' % new_path)
+  # An update is necessary.  Construct the rsync command.
+  update_port = shopfloor_client.GetUpdatePort()
+  new_path = os.path.join(parent_dir, 'updater.new')
+  RunRsync(
+    'rsync',
+    '-a', '--delete', '--stats',
+    # Use copies of identical files from the old autotest
+    # as much as possible to save network bandwidth.
+    '--copy-dest=%s' % parent_dir,
+    'rsync://%s:%d/factory/%s/' % (
+      urlparse(url).hostname,
+      update_port,
+      new_md5sum),
+    '%s/' % new_path)
 
-    CheckCriticalFiles(new_path)
+  CheckCriticalFiles(new_path)
 
-    new_md5sum_path = os.path.join(new_path, 'factory', 'MD5SUM')
-    new_md5sum_from_fs = open(new_md5sum_path).read().strip()
-    if new_md5sum != new_md5sum_from_fs:
-        raise UpdaterException(
-            'Unexpected MD5SUM in %s: expected %s but found %s' %
-            new_md5sum_path, new_md5sum, new_md5sum_from_fs)
+  new_md5sum_path = os.path.join(new_path, 'factory', 'MD5SUM')
+  new_md5sum_from_fs = open(new_md5sum_path).read().strip()
+  if new_md5sum != new_md5sum_from_fs:
+    raise UpdaterException(
+      'Unexpected MD5SUM in %s: expected %s but found %s' %
+      new_md5sum_path, new_md5sum, new_md5sum_from_fs)
 
-    if factory.in_chroot():
-        raise UpdaterException('Aborting update: In chroot')
+  if factory.in_chroot():
+    raise UpdaterException('Aborting update: In chroot')
 
-    # Alright, here we go!  This is the point of no return.
-    if pre_update_hook:
-        pre_update_hook()
+  # Alright, here we go!  This is the point of no return.
+  if pre_update_hook:
+    pre_update_hook()
 
-    old_path = os.path.join(parent_dir, 'updater.old.%s' % uuid.uuid4())
-    # If one of these fails, we're screwed.
-    for d in ['factory', 'autotest']:
-        shutil.move(os.path.join(parent_dir, d), old_path)
-        shutil.move(os.path.join(new_path, d), parent_dir)
-    # Delete the old and new trees
-    shutil.rmtree(old_path, ignore_errors=True)
-    shutil.rmtree(new_path, ignore_errors=True)
-    factory.console.info('Update successful')
-    return True
+  old_path = os.path.join(parent_dir, 'updater.old.%s' % uuid.uuid4())
+  # If one of these fails, we're screwed.
+  for d in ['factory', 'autotest']:
+    shutil.move(os.path.join(parent_dir, d), old_path)
+    shutil.move(os.path.join(new_path, d), parent_dir)
+  # Delete the old and new trees
+  shutil.rmtree(old_path, ignore_errors=True)
+  shutil.rmtree(new_path, ignore_errors=True)
+  factory.console.info('Update successful')
+  return True
diff --git a/py/goofy/web_socket_manager.py b/py/goofy/web_socket_manager.py
index ae9b641..3b58feb 100644
--- a/py/goofy/web_socket_manager.py
+++ b/py/goofy/web_socket_manager.py
@@ -20,167 +20,167 @@
 
 
 class WebSocketManager(object):
-    '''Object to manage web sockets for Goofy.
+  '''Object to manage web sockets for Goofy.
 
-    Brokers between events in the event client infrastructure
-    and on web sockets.  Also tails the console log and sends
-    events on web sockets when new bytes become available.
+  Brokers between events in the event client infrastructure
+  and on web sockets.  Also tails the console log and sends
+  events on web sockets when new bytes become available.
 
-    Each Goofy instance is associated with a UUID.  When a new web
-    socket is created, we send a hello event on the socket with the
-    current UUID.  If we receive a keepalive event with the wrong
-    UUID, we disconnect the client.  This insures that we are always
-    talking to a client that has a complete picture of our state
-    (i.e., if the server restarts, the client must restart as well).
+  Each Goofy instance is associated with a UUID.  When a new web
+  socket is created, we send a hello event on the socket with the
+  current UUID.  If we receive a keepalive event with the wrong
+  UUID, we disconnect the client.  This insures that we are always
+  talking to a client that has a complete picture of our state
+  (i.e., if the server restarts, the client must restart as well).
+  '''
+  def __init__(self, uuid):
+    self.uuid = uuid
+    self.lock = threading.Lock()
+    self.web_sockets = set()
+    self.event_client = None
+    self.tail_process = None
+    self.has_confirmed_socket = threading.Event()
+
+    self.event_client = EventClient(callback=self._handle_event,
+                    name='WebSocketManager')
+    self.tail_process = subprocess.Popen(
+      ["tail", "-F", factory.CONSOLE_LOG_PATH],
+      stdout=subprocess.PIPE,
+      close_fds=True)
+    self.tail_thread = threading.Thread(target=self._tail_console)
+    self.tail_thread.start()
+    self.closed = False
+
+  def close(self):
+    with self.lock:
+      if self.closed:
+        return
+      self.closed = True
+
+    if self.event_client:
+      self.event_client.close()
+      self.event_client = None
+
+    with self.lock:
+      web_sockets = list(self.web_sockets)
+    for web_socket in web_sockets:
+      web_socket.close_connection()
+
+    if self.tail_process:
+      self.tail_process.kill()
+      self.tail_process.wait()
+    if self.tail_thread:
+      self.tail_thread.join()
+
+  def has_sockets(self):
+    '''Returns true if any web sockets are currently connected.'''
+    with self.lock:
+      return len(self.web_sockets) > 0
+
+  def handle_web_socket(self, request):
+    '''Runs a web socket in the current thread.
+
+    request: A RequestHandler object containing the request.
     '''
-    def __init__(self, uuid):
-        self.uuid = uuid
-        self.lock = threading.Lock()
-        self.web_sockets = set()
-        self.event_client = None
-        self.tail_process = None
-        self.has_confirmed_socket = threading.Event()
+    def send_error(msg):
+      logging.error('Unable to start WebSocket connection: %s', msg)
+      request.send_response(400, msg)
 
-        self.event_client = EventClient(callback=self._handle_event,
-                                        name='WebSocketManager')
-        self.tail_process = subprocess.Popen(
-            ["tail", "-F", factory.CONSOLE_LOG_PATH],
-            stdout=subprocess.PIPE,
-            close_fds=True)
-        self.tail_thread = threading.Thread(target=self._tail_console)
-        self.tail_thread.start()
-        self.closed = False
+    encoded_key = request.headers.get('Sec-WebSocket-Key')
 
-    def close(self):
-        with self.lock:
-            if self.closed:
-                return
-            self.closed = True
+    if (request.headers.get('Upgrade') != 'websocket' or
+      request.headers.get('Connection') != 'Upgrade' or
+      not encoded_key):
+      send_error('Missing/unexpected headers in WebSocket request')
+      return
 
-        if self.event_client:
-            self.event_client.close()
-            self.event_client = None
+    key = base64.b64decode(encoded_key)
+    # Make sure the key is 16 characters, as required by the
+    # WebSockets spec (RFC6455).
+    if len(key) != 16:
+      send_error('Invalid key length')
 
-        with self.lock:
-            web_sockets = list(self.web_sockets)
-        for web_socket in web_sockets:
-            web_socket.close_connection()
+    version = request.headers.get('Sec-WebSocket-Version')
+    if not version or version not in [str(x) for x in ws4py.WS_VERSION]:
+      send_error('Unsupported WebSocket version %s' % version)
+      return
 
-        if self.tail_process:
-            self.tail_process.kill()
-            self.tail_process.wait()
-        if self.tail_thread:
-            self.tail_thread.join()
+    request.send_response(httplib.SWITCHING_PROTOCOLS)
+    request.send_header('Upgrade', 'websocket')
+    request.send_header('Connection', 'Upgrade')
+    request.send_header(
+      'Sec-WebSocket-Accept',
+      base64.b64encode(sha1(encoded_key + ws4py.WS_KEY).digest()))
+    request.end_headers()
 
-    def has_sockets(self):
-        '''Returns true if any web sockets are currently connected.'''
-        with self.lock:
-            return len(self.web_sockets) > 0
+    class MyWebSocket(WebSocket):
+      def received_message(socket_self, message):
+        event = Event.from_json(str(message))
+        if event.type == Event.Type.KEEPALIVE:
+          if event.uuid == self.uuid:
+            if not self.has_confirmed_socket.is_set():
+              logging.info('Chrome UI has come up')
+            self.has_confirmed_socket.set()
+          else:
+            logging.warning('Disconnecting web socket with '
+                    'incorrect UUID')
+            socket_self.close_connection()
+        else:
+          self.event_client.post_event(event)
 
-    def handle_web_socket(self, request):
-        '''Runs a web socket in the current thread.
+    web_socket = MyWebSocket(sock=request.connection)
 
-        request: A RequestHandler object containing the request.
-        '''
-        def send_error(msg):
-            logging.error('Unable to start WebSocket connection: %s', msg)
-            request.send_response(400, msg)
+    # Add a per-socket lock to use for sending, since ws4py is not
+    # thread-safe.
+    web_socket.send_lock = threading.Lock()
+    with web_socket.send_lock:
+      web_socket.send(Event(Event.Type.HELLO,
+                  uuid=self.uuid).to_json())
 
-        encoded_key = request.headers.get('Sec-WebSocket-Key')
+    try:
+      with self.lock:
+        self.web_sockets.add(web_socket)
+      logging.info('Running web socket')
+      web_socket.run()
+      logging.info('Web socket closed gracefully')
+    except:
+      logging.exception('Web socket closed with exception')
+    finally:
+      with self.lock:
+        self.web_sockets.discard(web_socket)
 
-        if (request.headers.get('Upgrade') != 'websocket' or
-            request.headers.get('Connection') != 'Upgrade' or
-            not encoded_key):
-            send_error('Missing/unexpected headers in WebSocket request')
-            return
+  def wait(self):
+    '''Waits for one socket to connect successfully.'''
+    self.has_confirmed_socket.wait()
 
-        key = base64.b64decode(encoded_key)
-        # Make sure the key is 16 characters, as required by the
-        # WebSockets spec (RFC6455).
-        if len(key) != 16:
-            send_error('Invalid key length')
+  def _tail_console(self):
+    '''Tails the console log, generating an event whenever a new
+    line is available.
 
-        version = request.headers.get('Sec-WebSocket-Version')
-        if not version or version not in [str(x) for x in ws4py.WS_VERSION]:
-            send_error('Unsupported WebSocket version %s' % version)
-            return
+    We send this event only to web sockets (not to event clients
+    in general) since only the UI is interested in these log
+    lines.
+    '''
+    while True:
+      line = self.tail_process.stdout.readline()
+      if line == '':
+        break
+      self._handle_event(
+        Event(Event.Type.LOG,
+            message=line.rstrip("\n")))
 
-        request.send_response(httplib.SWITCHING_PROTOCOLS)
-        request.send_header('Upgrade', 'websocket')
-        request.send_header('Connection', 'Upgrade')
-        request.send_header(
-            'Sec-WebSocket-Accept',
-            base64.b64encode(sha1(encoded_key + ws4py.WS_KEY).digest()))
-        request.end_headers()
+  def _handle_event(self, event):
+    '''Sends an event to each open WebSocket client.'''
+    with self.lock:
+      web_sockets = list(self.web_sockets)
 
-        class MyWebSocket(WebSocket):
-            def received_message(socket_self, message):
-                event = Event.from_json(str(message))
-                if event.type == Event.Type.KEEPALIVE:
-                    if event.uuid == self.uuid:
-                        if not self.has_confirmed_socket.is_set():
-                            logging.info('Chrome UI has come up')
-                        self.has_confirmed_socket.set()
-                    else:
-                        logging.warning('Disconnecting web socket with '
-                                        'incorrect UUID')
-                        socket_self.close_connection()
-                else:
-                    self.event_client.post_event(event)
+    if not web_sockets:
+      return
 
-        web_socket = MyWebSocket(sock=request.connection)
-
-        # Add a per-socket lock to use for sending, since ws4py is not
-        # thread-safe.
-        web_socket.send_lock = threading.Lock()
+    event_json = event.to_json()
+    for web_socket in web_sockets:
+      try:
         with web_socket.send_lock:
-            web_socket.send(Event(Event.Type.HELLO,
-                                  uuid=self.uuid).to_json())
-
-        try:
-            with self.lock:
-                self.web_sockets.add(web_socket)
-            logging.info('Running web socket')
-            web_socket.run()
-            logging.info('Web socket closed gracefully')
-        except:
-            logging.exception('Web socket closed with exception')
-        finally:
-            with self.lock:
-                self.web_sockets.discard(web_socket)
-
-    def wait(self):
-        '''Waits for one socket to connect successfully.'''
-        self.has_confirmed_socket.wait()
-
-    def _tail_console(self):
-        '''Tails the console log, generating an event whenever a new
-        line is available.
-
-        We send this event only to web sockets (not to event clients
-        in general) since only the UI is interested in these log
-        lines.
-        '''
-        while True:
-            line = self.tail_process.stdout.readline()
-            if line == '':
-                break
-            self._handle_event(
-                Event(Event.Type.LOG,
-                      message=line.rstrip("\n")))
-
-    def _handle_event(self, event):
-        '''Sends an event to each open WebSocket client.'''
-        with self.lock:
-            web_sockets = list(self.web_sockets)
-
-        if not web_sockets:
-            return
-
-        event_json = event.to_json()
-        for web_socket in web_sockets:
-            try:
-                with web_socket.send_lock:
-                    web_socket.send(event_json)
-            except:
-                logging.exception('Unable to send event on web socket')
+          web_socket.send(event_json)
+      except:
+        logging.exception('Unable to send event on web socket')
diff --git a/py/test/event.py b/py/test/event.py
index 80a471b..98d9476 100644
--- a/py/test/event.py
+++ b/py/test/event.py
@@ -17,7 +17,7 @@
 import types
 from Queue import Empty, Queue
 
-import factory_common
+import factory_common # pylint: disable=W0611
 from cros.factory.test import factory
 from cros.factory.test.unicode_to_string import UnicodeToString
 
@@ -25,423 +25,423 @@
 # Environment variable storing the path to the endpoint.
 CROS_FACTORY_EVENT = 'CROS_FACTORY_EVENT'
 
-# Maximum allowed size for messages.  If messages are bigger than this, they
+# Maximum allowed size for messages. If messages are bigger than this, they
 # will be truncated by the seqpacket sockets.
 _MAX_MESSAGE_SIZE = 65535
 
 
 def json_default_repr(obj):
-    '''Converts an object into a suitable representation for
-    JSON-ification.
+  '''Converts an object into a suitable representation for
+  JSON-ification.
 
-    If obj is an object, this returns a dict with all properties
-    not beginning in '_'.  Otherwise, the original object is
-    returned.
-    '''
-    if isinstance(obj, object):
-        return dict([(k,v) for k, v in obj.__dict__.iteritems()
-                     if k[0] != "_"])
-    else:
-        return obj
+  If obj is an object, this returns a dict with all properties
+  not beginning in '_'. Otherwise, the original object is
+  returned.
+  '''
+  if isinstance(obj, object):
+    return dict([(k,v) for k, v in obj.__dict__.iteritems()
+           if k[0] != "_"])
+  else:
+    return obj
 
 
 class Event(object):
-    '''
-    An event object that may be written to the event server.
+  '''
+  An event object that may be written to the event server.
 
-    E.g.:
+  E.g.:
 
-        event = Event(Event.Type.STATE_CHANGE,
-                      test='foo.bar',
-                      state=TestState(...))
-    '''
-    Type = type('Event.Type', (), {
-            # The state of a test has changed.
-            'STATE_CHANGE':          'goofy:state_change',
-            # The UI has come up.
-            'UI_READY':              'goofy:ui_ready',
-            # Tells goofy to switch to a new test.
-            'SWITCH_TEST':           'goofy:switch_test',
-            # Tells goofy to rotate visibility to the next active test.
-            'SHOW_NEXT_ACTIVE_TEST': 'goofy:show_next_active_test',
-            # Tells goofy to show a particular test.
-            'SET_VISIBLE_TEST':      'goofy:set_visible_test',
-            # Tells goofy to clear all state and restart testing.
-            'RESTART_TESTS':  'goofy:restart_tests',
-            # Tells goofy to run all tests that haven't been run yet.
-            'AUTO_RUN': 'goofy:auto_run',
-            # Tells goofy to set all failed tests' state to untested and re-run.
-            'RE_RUN_FAILED': 'goofy:re_run_failed',
-            # Tells goofy to re-run all tests with particular statuses.
-            'RUN_TESTS_WITH_STATUS': 'goofy:run_tests_with_status',
-            # Tells goofy to go to the review screen.
-            'REVIEW': 'goofy:review',
-            # Tells the UI about a single new line in the log.
-            'LOG': 'goofy:log',
-            # A hello message to a new WebSocket.  Contains a 'uuid' parameter
-            # identification the particular invocation of the server.
-            'HELLO': 'goofy:hello',
-            # A keepalive message from the UI.  Contains a 'uuid' parameter
-            # containing the same 'uuid' value received when the client received
-            # its HELLO.
-            'KEEPALIVE': 'goofy:keepalive',
-            # Sets the UI in the test pane.
-            'SET_HTML': 'goofy:set_html',
-            # Runs JavaScript in the test pane.
-            'RUN_JS': 'goofy:run_js',
-            # Calls a JavaScript function in the test pane.
-            'CALL_JS_FUNCTION': 'goofy:call_js_function',
-            # Event from a test UI.
-            'TEST_UI_EVENT': 'goofy:test_ui_event',
-            # Message from the test UI that it has finished.
-            'END_TEST': 'goofy:end_test',
-            # Message to tell the test UI to destroy itself.
-            'DESTROY_TEST': 'goofy:destroy_test',
-            # Message telling Goofy should re-read system info.
-            'UPDATE_SYSTEM_INFO': 'goofy:update_system_info',
-            # Message containing new system info from Goofy.
-            'SYSTEM_INFO': 'goofy:system_info',
-            # Tells Goofy to stop all tests and update factory
-            # software.
-            'UPDATE_FACTORY': 'goofy:update_factory',
-            # Tells Goofy to stop all tests.
-            'STOP': 'goofy:stop',
-            # Indicates a pending shutdown.
-            'PENDING_SHUTDOWN': 'goofy:pending_shutdown',
-            # Cancels a pending shutdown.
-            'CANCEL_SHUTDOWN': 'goofy:cancel_shutdown',
-            })
+    event = Event(Event.Type.STATE_CHANGE,
+           test='foo.bar',
+           state=TestState(...))
+  '''
+  Type = type('Event.Type', (), {
+      # The state of a test has changed.
+      'STATE_CHANGE':     'goofy:state_change',
+      # The UI has come up.
+      'UI_READY':       'goofy:ui_ready',
+      # Tells goofy to switch to a new test.
+      'SWITCH_TEST':      'goofy:switch_test',
+      # Tells goofy to rotate visibility to the next active test.
+      'SHOW_NEXT_ACTIVE_TEST': 'goofy:show_next_active_test',
+      # Tells goofy to show a particular test.
+      'SET_VISIBLE_TEST':   'goofy:set_visible_test',
+      # Tells goofy to clear all state and restart testing.
+      'RESTART_TESTS': 'goofy:restart_tests',
+      # Tells goofy to run all tests that haven't been run yet.
+      'AUTO_RUN': 'goofy:auto_run',
+      # Tells goofy to set all failed tests' state to untested and re-run.
+      'RE_RUN_FAILED': 'goofy:re_run_failed',
+      # Tells goofy to re-run all tests with particular statuses.
+      'RUN_TESTS_WITH_STATUS': 'goofy:run_tests_with_status',
+      # Tells goofy to go to the review screen.
+      'REVIEW': 'goofy:review',
+      # Tells the UI about a single new line in the log.
+      'LOG': 'goofy:log',
+      # A hello message to a new WebSocket. Contains a 'uuid' parameter
+      # identification the particular invocation of the server.
+      'HELLO': 'goofy:hello',
+      # A keepalive message from the UI. Contains a 'uuid' parameter
+      # containing the same 'uuid' value received when the client received
+      # its HELLO.
+      'KEEPALIVE': 'goofy:keepalive',
+      # Sets the UI in the test pane.
+      'SET_HTML': 'goofy:set_html',
+      # Runs JavaScript in the test pane.
+      'RUN_JS': 'goofy:run_js',
+      # Calls a JavaScript function in the test pane.
+      'CALL_JS_FUNCTION': 'goofy:call_js_function',
+      # Event from a test UI.
+      'TEST_UI_EVENT': 'goofy:test_ui_event',
+      # Message from the test UI that it has finished.
+      'END_TEST': 'goofy:end_test',
+      # Message to tell the test UI to destroy itself.
+      'DESTROY_TEST': 'goofy:destroy_test',
+      # Message telling Goofy should re-read system info.
+      'UPDATE_SYSTEM_INFO': 'goofy:update_system_info',
+      # Message containing new system info from Goofy.
+      'SYSTEM_INFO': 'goofy:system_info',
+      # Tells Goofy to stop all tests and update factory
+      # software.
+      'UPDATE_FACTORY': 'goofy:update_factory',
+      # Tells Goofy to stop all tests.
+      'STOP': 'goofy:stop',
+      # Indicates a pending shutdown.
+      'PENDING_SHUTDOWN': 'goofy:pending_shutdown',
+      # Cancels a pending shutdown.
+      'CANCEL_SHUTDOWN': 'goofy:cancel_shutdown',
+      })
 
-    def __init__(self, type, **kw):  # pylint: disable=W0622
-        self.type = type
-        self.timestamp = time.time()
-        for k, v in kw.iteritems():
-            setattr(self, k, v)
+  def __init__(self, type, **kw): # pylint: disable=W0622
+    self.type = type
+    self.timestamp = time.time()
+    for k, v in kw.iteritems():
+      setattr(self, k, v)
 
-    def __repr__(self):
-        return factory.std_repr(
-            self,
-            extra=[
-                'type=%s' % self.type,
-                'timestamp=%s' % time.ctime(self.timestamp)],
-            excluded_keys=['type', 'timestamp'])
+  def __repr__(self):
+    return factory.std_repr(
+      self,
+      extra=[
+        'type=%s' % self.type,
+        'timestamp=%s' % time.ctime(self.timestamp)],
+      excluded_keys=['type', 'timestamp'])
 
-    def to_json(self):
-        return json.dumps(self, default=json_default_repr)
+  def to_json(self):
+    return json.dumps(self, default=json_default_repr)
 
-    @staticmethod
-    def from_json(encoded_event):
-        kw = UnicodeToString(json.loads(encoded_event))
-        type = kw.pop('type')
-        return Event(type=type, **kw)
+  @staticmethod
+  def from_json(encoded_event):
+    kw = UnicodeToString(json.loads(encoded_event))
+    type = kw.pop('type')
+    return Event(type=type, **kw)
 
 _unique_id_lock = threading.Lock()
 _unique_id = 1
 def get_unique_id():
-    global _unique_id
-    with _unique_id_lock:
-        ret = _unique_id
-        _unique_id += 1
-    return ret
+  global _unique_id
+  with _unique_id_lock:
+    ret = _unique_id
+    _unique_id += 1
+  return ret
 
 
 class EventServerRequestHandler(SocketServer.BaseRequestHandler):
-    '''
-    Request handler for the event server.
+  '''
+  Request handler for the event server.
 
-    This class is agnostic to message format (except for logging).
-    '''
-    # pylint: disable=W0201,W0212
-    def setup(self):
-        SocketServer.BaseRequestHandler.setup(self)
-        threading.current_thread().name = (
-            'EventServerRequestHandler-%d' % get_unique_id())
-        # A thread to be used to send messages that are posted to the queue.
-        self.send_thread = None
-        # A queue containing messages.
-        self.queue = Queue()
+  This class is agnostic to message format (except for logging).
+  '''
+  # pylint: disable=W0201,W0212
+  def setup(self):
+    SocketServer.BaseRequestHandler.setup(self)
+    threading.current_thread().name = (
+      'EventServerRequestHandler-%d' % get_unique_id())
+    # A thread to be used to send messages that are posted to the queue.
+    self.send_thread = None
+    # A queue containing messages.
+    self.queue = Queue()
 
-    def handle(self):
-        # The handle() methods is run in a separate thread per client
-        # (since EventServer has ThreadingMixIn).
-        logging.debug('Event server: handling new client')
-        try:
-            self.server._subscribe(self.queue)
+  def handle(self):
+    # The handle() methods is run in a separate thread per client
+    # (since EventServer has ThreadingMixIn).
+    logging.debug('Event server: handling new client')
+    try:
+      self.server._subscribe(self.queue)
 
-            self.send_thread = threading.Thread(
-                target=self._run_send_thread,
-                name='EventServerSendThread-%d' % get_unique_id())
-            self.send_thread.daemon = True
-            self.send_thread.start()
+      self.send_thread = threading.Thread(
+        target=self._run_send_thread,
+        name='EventServerSendThread-%d' % get_unique_id())
+      self.send_thread.daemon = True
+      self.send_thread.start()
 
-            # Process events: continuously read message and broadcast to all
-            # clients' queues.
-            while True:
-                msg = self.request.recv(_MAX_MESSAGE_SIZE + 1)
-                if len(msg) > _MAX_MESSAGE_SIZE:
-                    logging.error('Event server: message too large')
-                if len(msg) == 0:
-                    break  # EOF
-                self.server._post_message(msg)
-        except socket.error, e:
-            if e.errno in [errno.ECONNRESET, errno.ESHUTDOWN]:
-                pass  # Client just quit
-            else:
-                raise e
-        finally:
-            logging.debug('Event server: client disconnected')
-            self.queue.put(None)  # End of stream; make writer quit
-            self.server._unsubscribe(self.queue)
+      # Process events: continuously read message and broadcast to all
+      # clients' queues.
+      while True:
+        msg = self.request.recv(_MAX_MESSAGE_SIZE + 1)
+        if len(msg) > _MAX_MESSAGE_SIZE:
+          logging.error('Event server: message too large')
+        if len(msg) == 0:
+          break # EOF
+        self.server._post_message(msg)
+    except socket.error, e:
+      if e.errno in [errno.ECONNRESET, errno.ESHUTDOWN]:
+        pass # Client just quit
+      else:
+        raise e
+    finally:
+      logging.debug('Event server: client disconnected')
+      self.queue.put(None) # End of stream; make writer quit
+      self.server._unsubscribe(self.queue)
 
-    def _run_send_thread(self):
-        while True:
-            message = self.queue.get()
-            if message is None:
-                return
-            try:
-                self.request.send(message)
-            except:  # pylint: disable=W0702
-                return
+  def _run_send_thread(self):
+    while True:
+      message = self.queue.get()
+      if message is None:
+        return
+      try:
+        self.request.send(message)
+      except: # pylint: disable=W0702
+        return
 
 
 class EventServer(SocketServer.ThreadingUnixStreamServer):
+  '''
+  An event server that broadcasts messages to all clients.
+
+  This class is agnostic to message format (except for logging).
+  '''
+  allow_reuse_address = True
+  socket_type = socket.SOCK_SEQPACKET
+  daemon_threads = True
+
+  def __init__(self, path=None):
     '''
-    An event server that broadcasts messages to all clients.
+    Constructor.
 
-    This class is agnostic to message format (except for logging).
+    @param path: Path at which to create a UNIX stream socket.
+      If None, uses a temporary path and sets the CROS_FACTORY_EVENT
+      environment variable for future clients to use.
     '''
-    allow_reuse_address = True
-    socket_type = socket.SOCK_SEQPACKET
-    daemon_threads = True
+    # A set of queues listening to messages.
+    self._queues = set()
+    # A lock guarding the _queues variable.
+    self._lock = threading.Lock()
+    if not path:
+      path = tempfile.mktemp(prefix='cros_factory_event.')
+      os.environ[CROS_FACTORY_EVENT] = path
+      logging.info('Setting %s=%s', CROS_FACTORY_EVENT, path)
+    SocketServer.UnixStreamServer.__init__( # pylint: disable=W0233
+      self, path, EventServerRequestHandler)
 
-    def __init__(self, path=None):
-        '''
-        Constructor.
+  def _subscribe(self, queue):
+    '''
+    Subscribes a queue to receive events.
 
-        @param path: Path at which to create a UNIX stream socket.
-            If None, uses a temporary path and sets the CROS_FACTORY_EVENT
-            environment variable for future clients to use.
-        '''
-        # A set of queues listening to messages.
-        self._queues = set()
-        # A lock guarding the _queues variable.
-        self._lock = threading.Lock()
-        if not path:
-            path = tempfile.mktemp(prefix='cros_factory_event.')
-            os.environ[CROS_FACTORY_EVENT] = path
-            logging.info('Setting %s=%s', CROS_FACTORY_EVENT, path)
-        SocketServer.UnixStreamServer.__init__(  # pylint: disable=W0233
-            self, path, EventServerRequestHandler)
+    Invoked only from the request handler.
+    '''
+    with self._lock:
+      self._queues.add(queue)
 
-    def _subscribe(self, queue):
-        '''
-        Subscribes a queue to receive events.
+  def _unsubscribe(self, queue):
+    '''
+    Unsubscribes a queue to receive events.
 
-        Invoked only from the request handler.
-        '''
-        with self._lock:
-            self._queues.add(queue)
+    Invoked only from the request handler.
+    '''
+    with self._lock:
+      self._queues.discard(queue)
 
-    def _unsubscribe(self, queue):
-        '''
-        Unsubscribes a queue to receive events.
+  def _post_message(self, message):
+    '''
+    Posts a message to all clients.
 
-        Invoked only from the request handler.
-        '''
-        with self._lock:
-            self._queues.discard(queue)
+    Invoked only from the request handler.
+    '''
+    try:
+      if logging.getLogger().isEnabledFor(logging.DEBUG):
+        logging.debug('Event server: dispatching object %s',
+               pickle.loads(message))
+    except: # pylint: disable=W0702
+      # Message isn't parseable as a pickled object; weird!
+      logging.info(
+        'Event server: dispatching message %r', message)
 
-    def _post_message(self, message):
-        '''
-        Posts a message to all clients.
-
-        Invoked only from the request handler.
-        '''
-        try:
-            if logging.getLogger().isEnabledFor(logging.DEBUG):
-                logging.debug('Event server: dispatching object %s',
-                              pickle.loads(message))
-        except:  # pylint: disable=W0702
-            # Message isn't parseable as a pickled object; weird!
-            logging.info(
-                'Event server: dispatching message %r', message)
-
-        with self._lock:
-            for q in self._queues:
-                # Note that this is nonblocking (even if one of the
-                # clients is dead).
-                q.put(message)
+    with self._lock:
+      for q in self._queues:
+        # Note that this is nonblocking (even if one of the
+        # clients is dead).
+        q.put(message)
 
 
 class EventClient(object):
-    EVENT_LOOP_GOBJECT_IDLE = 'EVENT_LOOP_GOBJECT_IDLE'
-    EVENT_LOOP_GOBJECT_IO = 'EVENT_LOOP_GOBJECT_IO'
+  EVENT_LOOP_GOBJECT_IDLE = 'EVENT_LOOP_GOBJECT_IDLE'
+  EVENT_LOOP_GOBJECT_IO = 'EVENT_LOOP_GOBJECT_IO'
 
+  '''
+  A client used to post and receive messages from an event server.
+
+  All events sent through this class must be subclasses of Event. It
+  marshals Event classes through the server by pickling them.
+  '''
+  def __init__(self, path=None, callback=None, event_loop=None, name=None):
     '''
-    A client used to post and receive messages from an event server.
+    Constructor.
 
-    All events sent through this class must be subclasses of Event.  It
-    marshals Event classes through the server by pickling them.
+    @param path: The UNIX seqpacket socket endpoint path. If None, uses
+      the CROS_FACTORY_EVENT environment variable.
+    @param callback: A callback to call when events occur. The callback
+      takes one argument: the received event.
+    @param event_loop: An event loop to use to post the events. May be one
+      of:
+
+      - A Queue object, in which case a lambda invoking the callback is
+       written to the queue.
+      - EVENT_LOOP_GOBJECT_IDLE, in which case the callback will be
+       invoked in the gobject event loop using idle_add.
+      - EVENT_LOOP_GOBJECT_IO, in which case the callback will be
+       invoked from an async IO handler.
+    @param name: An optional name for the client
     '''
-    def __init__(self, path=None, callback=None, event_loop=None, name=None):
-        '''
-        Constructor.
+    self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
+    self.callbacks = set()
+    logging.debug('Initializing event client')
 
-        @param path: The UNIX seqpacket socket endpoint path.  If None, uses
-            the CROS_FACTORY_EVENT environment variable.
-        @param callback: A callback to call when events occur.  The callback
-            takes one argument: the received event.
-        @param event_loop: An event loop to use to post the events.  May be one
-            of:
+    should_start_thread = True
 
-            - A Queue object, in which case a lambda invoking the callback is
-              written to the queue.
-            - EVENT_LOOP_GOBJECT_IDLE, in which case the callback will be
-              invoked in the gobject event loop using idle_add.
-            - EVENT_LOOP_GOBJECT_IO, in which case the callback will be
-              invoked from an async IO handler.
-        @param name: An optional name for the client
-        '''
-        self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
-        self.callbacks = set()
-        logging.debug('Initializing event client')
+    path = path or os.environ[CROS_FACTORY_EVENT]
+    self.socket.connect(path)
+    self._lock = threading.Lock()
 
-        should_start_thread = True
+    if callback:
+      if isinstance(event_loop, Queue):
+        self.callbacks.add(
+          lambda event: event_loop.put(
+            lambda: callback(event)))
+      elif event_loop == self.EVENT_LOOP_GOBJECT_IDLE:
+        import gobject
+        self.callbacks.add(
+          lambda event: gobject.idle_add(callback, event))
+      elif event_loop == self.EVENT_LOOP_GOBJECT_IO:
+        import gobject
+        should_start_thread = False
+        gobject.io_add_watch(
+          self.socket, gobject.IO_IN,
+          lambda source, condition: self._read_one_message())
+        self.callbacks.add(callback)
+      else:
+        self.callbacks.add(callback)
 
-        path = path or os.environ[CROS_FACTORY_EVENT]
-        self.socket.connect(path)
-        self._lock = threading.Lock()
+    if should_start_thread:
+      self.recv_thread = threading.Thread(
+        target=self._run_recv_thread,
+        name='EventServerRecvThread-%s' % (name or get_unique_id()))
+      self.recv_thread.daemon = True
+      self.recv_thread.start()
+    else:
+      self.recv_thread = None
 
-        if callback:
-            if isinstance(event_loop, Queue):
-                self.callbacks.add(
-                    lambda event: event_loop.put(
-                        lambda: callback(event)))
-            elif event_loop == self.EVENT_LOOP_GOBJECT_IDLE:
-                import gobject
-                self.callbacks.add(
-                    lambda event: gobject.idle_add(callback, event))
-            elif event_loop == self.EVENT_LOOP_GOBJECT_IO:
-                import gobject
-                should_start_thread = False
-                gobject.io_add_watch(
-                    self.socket, gobject.IO_IN,
-                    lambda source, condition: self._read_one_message())
-                self.callbacks.add(callback)
-            else:
-                self.callbacks.add(callback)
+  def close(self):
+    '''Closes the client, waiting for any threads to terminate.'''
+    if not self.socket:
+      return
 
-        if should_start_thread:
-            self.recv_thread = threading.Thread(
-                target=self._run_recv_thread,
-                name='EventServerRecvThread-%s' % (name or get_unique_id()))
-            self.recv_thread.daemon = True
-            self.recv_thread.start()
-        else:
-            self.recv_thread = None
+    # Shutdown the socket to cause recv_thread to terminate.
+    self.socket.shutdown(socket.SHUT_RDWR)
+    if self.recv_thread:
+      self.recv_thread.join()
+    self.socket.close()
+    self.socket = None
 
-    def close(self):
-        '''Closes the client, waiting for any threads to terminate.'''
-        if not self.socket:
-            return
+  def __del__(self):
+    self.close()
 
-        # Shutdown the socket to cause recv_thread to terminate.
-        self.socket.shutdown(socket.SHUT_RDWR)
-        if self.recv_thread:
-            self.recv_thread.join()
-        self.socket.close()
-        self.socket = None
+  def __enter__(self):
+    return self
 
-    def __del__(self):
-        self.close()
+  def __exit__(self, exc_type, exc_value, traceback):
+    try:
+      self.close()
+    except:
+      pass
+    return False
 
-    def __enter__(self):
-        return self
+  def post_event(self, event):
+    '''
+    Posts an event to the server.
+    '''
+    logging.debug('Event client: sending event %s', event)
+    message = pickle.dumps(event, protocol=2)
+    if len(message) > _MAX_MESSAGE_SIZE:
+      # Log it first so we know what event caused the problem.
+      logging.error("Message too large (%d bytes): event is %s" %
+             (len(message), event))
+      raise IOError("Message too large (%d bytes)" % len(message))
+    self.socket.sendall(message)
 
-    def __exit__(self, exc_type, exc_value, traceback):
-        try:
-            self.close()
-        except:
-            pass
-        return False
+  def wait(self, condition, timeout=None):
+    '''
+    Waits for an event matching a condition.
 
-    def post_event(self, event):
-        '''
-        Posts an event to the server.
-        '''
-        logging.debug('Event client: sending event %s', event)
-        message = pickle.dumps(event, protocol=2)
-        if len(message) > _MAX_MESSAGE_SIZE:
-            # Log it first so we know what event caused the problem.
-            logging.error("Message too large (%d bytes): event is %s" %
-                          (len(message), event))
-            raise IOError("Message too large (%d bytes)" % len(message))
-        self.socket.sendall(message)
+    @param condition: A function to evaluate. The function takes one
+      argument (an event to evaluate) and returns whether the condition
+      applies.
+    @param timeout: A timeout in seconds. wait will return None on
+      timeout.
+    '''
+    queue = Queue()
 
-    def wait(self, condition, timeout=None):
-        '''
-        Waits for an event matching a condition.
+    def check_condition(event):
+      if condition(event):
+        queue.put(event)
 
-        @param condition: A function to evaluate.  The function takes one
-            argument (an event to evaluate) and returns whether the condition
-            applies.
-        @param timeout: A timeout in seconds.  wait will return None on
-            timeout.
-        '''
-        queue = Queue()
+    try:
+      with self._lock:
+        self.callbacks.add(check_condition)
+      return queue.get(timeout=timeout)
+    except Empty:
+      return None
+    finally:
+      with self._lock:
+        self.callbacks.remove(check_condition)
 
-        def check_condition(event):
-            if condition(event):
-                queue.put(event)
+  def _run_recv_thread(self):
+    '''
+    Thread to receive messages and broadcast them to callbacks.
+    '''
+    while self._read_one_message():
+      pass
 
-        try:
-            with self._lock:
-                self.callbacks.add(check_condition)
-            return queue.get(timeout=timeout)
-        except Empty:
-            return None
-        finally:
-            with self._lock:
-                self.callbacks.remove(check_condition)
+  def _read_one_message(self):
+    '''
+    Handles one incomming message from the socket.
 
-    def _run_recv_thread(self):
-        '''
-        Thread to receive messages and broadcast them to callbacks.
-        '''
-        while self._read_one_message():
-            pass
+    @return: True if event processing should continue (i.e., not EOF).
+    '''
+    bytes = self.socket.recv(_MAX_MESSAGE_SIZE + 1)
+    if len(bytes) > _MAX_MESSAGE_SIZE:
+      # The message may have been truncated - ignore it
+      logging.error('Event client: message too large')
+      return True
 
-    def _read_one_message(self):
-        '''
-        Handles one incomming message from the socket.
+    if len(bytes) == 0:
+      return False
 
-        @return: True if event processing should continue (i.e., not EOF).
-        '''
-        bytes = self.socket.recv(_MAX_MESSAGE_SIZE + 1)
-        if len(bytes) > _MAX_MESSAGE_SIZE:
-            # The message may have been truncated - ignore it
-            logging.error('Event client: message too large')
-            return True
+    try:
+      event = pickle.loads(bytes)
+      logging.debug('Event client: dispatching event %s', event)
+    except:
+      logging.warn('Event client: bad message %r', bytes)
+      traceback.print_exc(sys.stderr)
+      return True
 
-        if len(bytes) == 0:
-            return False
+    with self._lock:
+      callbacks = list(self.callbacks)
+    for callback in callbacks:
+      try:
+        callback(event)
+      except:
+        logging.warn('Event client: error in callback')
+        traceback.print_exc(sys.stderr)
+        # Keep going
 
-        try:
-            event = pickle.loads(bytes)
-            logging.debug('Event client: dispatching event %s', event)
-        except:
-            logging.warn('Event client: bad message %r', bytes)
-            traceback.print_exc(sys.stderr)
-            return True
-
-        with self._lock:
-            callbacks = list(self.callbacks)
-        for callback in callbacks:
-            try:
-                callback(event)
-            except:
-                logging.warn('Event client: error in callback')
-                traceback.print_exc(sys.stderr)
-                # Keep going
-
-        return True
+    return True
diff --git a/py/test/factory.py b/py/test/factory.py
index e505030..d7c15f2 100644
--- a/py/test/factory.py
+++ b/py/test/factory.py
@@ -5,13 +5,13 @@
 
 '''
 This library provides common types and routines for the factory test
-infrastructure.  This library explicitly does not import gtk, to
+infrastructure. This library explicitly does not import gtk, to
 allow its use by the autotest control process.
 
 To log to the factory console, use:
 
-  from cros.factory.test import factory
-  factory.console.info('...')  # Or warn, or error
+ from cros.factory.test import factory
+ factory.console.info('...') # Or warn, or error
 '''
 
 
@@ -20,7 +20,7 @@
 import os
 import sys
 
-import factory_common
+import factory_common # pylint: disable=W0611
 from cros.factory.goofy import connection_manager
 from cros.factory.test import utils
 
@@ -35,7 +35,7 @@
 
 
 class TestListError(Exception):
-    pass
+  pass
 
 
 # For compatibility; moved to utils.
@@ -43,23 +43,23 @@
 
 
 def get_log_root():
-    '''Returns the root for logging and state.
+  '''Returns the root for logging and state.
 
-    This is usually /var/log, or /tmp/factory.$USER if in the chroot, but may be
-    overridden by the CROS_FACTORY_LOG_ROOT environment variable.
-    '''
-    ret = os.environ.get('CROS_FACTORY_LOG_ROOT')
-    if ret:
-        return ret
-    if in_chroot():
-        return '/tmp/factory.%s' % getpass.getuser()
-    return '/var/log'
+  This is usually /var/log, or /tmp/factory.$USER if in the chroot, but may be
+  overridden by the CROS_FACTORY_LOG_ROOT environment variable.
+  '''
+  ret = os.environ.get('CROS_FACTORY_LOG_ROOT')
+  if ret:
+    return ret
+  if in_chroot():
+    return '/tmp/factory.%s' % getpass.getuser()
+  return '/var/log'
 
 
 def get_state_root():
-    '''Returns the root for all factory state.'''
-    return os.path.join(
-        get_log_root(), 'factory_state.v%d' % FACTORY_STATE_VERSION)
+  '''Returns the root for all factory state.'''
+  return os.path.join(
+    get_log_root(), 'factory_state.v%d' % FACTORY_STATE_VERSION)
 
 
 CONSOLE_LOG_PATH = os.path.join(get_log_root(), 'console.log')
@@ -70,701 +70,701 @@
 
 
 def get_current_test_path():
-    # Returns the path of the currently executing test, if any.
-    return os.environ.get("CROS_FACTORY_TEST_PATH")
+  # Returns the path of the currently executing test, if any.
+  return os.environ.get("CROS_FACTORY_TEST_PATH")
 
 
 def get_lsb_data():
-    """Reads all key-value pairs from system lsb-* configuration files."""
-    # TODO(hungte) Re-implement using regex.
-    # lsb-* file format:
-    # [#]KEY="VALUE DATA"
-    lsb_files = ('/etc/lsb-release',
-                 '/usr/local/etc/lsb-release',
-                 '/usr/local/etc/lsb-factory')
-    def unquote(entry):
-        for c in ('"', "'"):
-            if entry.startswith(c) and entry.endswith(c):
-                return entry[1:-1]
-        return entry
-    data = dict()
-    for lsb_file in lsb_files:
-        if not os.path.exists(lsb_file):
-            continue
-        with open(lsb_file, "rt") as lsb_handle:
-            for line in lsb_handle.readlines():
-                line = line.strip()
-                if ('=' not in line) or line.startswith('#'):
-                    continue
-                (key, value) = line.split('=', 1)
-                data[unquote(key)] = unquote(value)
-    return data
+  """Reads all key-value pairs from system lsb-* configuration files."""
+  # TODO(hungte) Re-implement using regex.
+  # lsb-* file format:
+  # [#]KEY="VALUE DATA"
+  lsb_files = ('/etc/lsb-release',
+         '/usr/local/etc/lsb-release',
+         '/usr/local/etc/lsb-factory')
+  def unquote(entry):
+    for c in ('"', "'"):
+      if entry.startswith(c) and entry.endswith(c):
+        return entry[1:-1]
+    return entry
+  data = dict()
+  for lsb_file in lsb_files:
+    if not os.path.exists(lsb_file):
+      continue
+    with open(lsb_file, "rt") as lsb_handle:
+      for line in lsb_handle.readlines():
+        line = line.strip()
+        if ('=' not in line) or line.startswith('#'):
+          continue
+        (key, value) = line.split('=', 1)
+        data[unquote(key)] = unquote(value)
+  return data
 
 
 def get_current_md5sum():
-    '''Returns MD5SUM of the current autotest directory.
+  '''Returns MD5SUM of the current autotest directory.
 
-    Returns None if there has been no update (i.e., unable to read
-    the MD5SUM file).
-    '''
-    if os.path.exists(FACTORY_MD5SUM_PATH):
-        return open(FACTORY_MD5SUM_PATH, 'r').read().strip()
-    else:
-        return None
+  Returns None if there has been no update (i.e., unable to read
+  the MD5SUM file).
+  '''
+  if os.path.exists(FACTORY_MD5SUM_PATH):
+    return open(FACTORY_MD5SUM_PATH, 'r').read().strip()
+  else:
+    return None
 
 
 def _init_console_log():
-    handler = logging.FileHandler(CONSOLE_LOG_PATH, "a", delay=True)
-    log_format = '[%(levelname)s] %(message)s'
-    test_path = get_current_test_path()
-    if test_path:
-        log_format = test_path + ': ' + log_format
-    handler.setFormatter(logging.Formatter(log_format))
+  handler = logging.FileHandler(CONSOLE_LOG_PATH, "a", delay=True)
+  log_format = '[%(levelname)s] %(message)s'
+  test_path = get_current_test_path()
+  if test_path:
+    log_format = test_path + ': ' + log_format
+  handler.setFormatter(logging.Formatter(log_format))
 
-    ret = logging.getLogger("console")
-    ret.addHandler(handler)
-    ret.setLevel(logging.INFO)
-    return ret
+  ret = logging.getLogger("console")
+  ret.addHandler(handler)
+  ret.setLevel(logging.INFO)
+  return ret
 
 
 console = _init_console_log()
 
 
 def std_repr(obj, extra=[], excluded_keys=[], true_only=False):
-    '''
-    Returns the representation of an object including its properties.
+  '''
+  Returns the representation of an object including its properties.
 
-    @param extra: Extra items to include in the representation.
-    @param excluded_keys: Keys not to include in the representation.
-    @param true_only: Whether to include only values that evaluate to
-        true.
-    '''
-    # pylint: disable=W0102
-    return (obj.__class__.__name__ + '(' +
-            ', '.join(extra +
-                      ['%s=%s' % (k, repr(getattr(obj, k)))
-                       for k in sorted(obj.__dict__.keys())
-                       if k[0] != '_' and k not in excluded_keys and
-                       (not true_only or getattr(obj, k))])
-            + ')')
+  @param extra: Extra items to include in the representation.
+  @param excluded_keys: Keys not to include in the representation.
+  @param true_only: Whether to include only values that evaluate to
+    true.
+  '''
+  # pylint: disable=W0102
+  return (obj.__class__.__name__ + '(' +
+      ', '.join(extra +
+           ['%s=%s' % (k, repr(getattr(obj, k)))
+            for k in sorted(obj.__dict__.keys())
+            if k[0] != '_' and k not in excluded_keys and
+            (not true_only or getattr(obj, k))])
+      + ')')
 
 
 def log(s):
-    '''
-    Logs a message to the console.  Deprecated; use the 'console'
-    property instead.
+  '''
+  Logs a message to the console. Deprecated; use the 'console'
+  property instead.
 
-    TODO(jsalz): Remove references throughout factory tests.
-    '''
-    console.info(s)
+  TODO(jsalz): Remove references throughout factory tests.
+  '''
+  console.info(s)
 
 
 def get_state_instance():
-    '''
-    Returns a cached factory state client instance.
-    '''
-    # Delay loading modules to prevent circular dependency.
-    import factory_common
-    from cros.factory.test import state
-    global _state_instance  # pylint: disable=W0603
-    if _state_instance is None:
-        _state_instance = state.get_instance()
-    return _state_instance
+  '''
+  Returns a cached factory state client instance.
+  '''
+  # Delay loading modules to prevent circular dependency.
+  import factory_common # pylint: disable=W0611
+  from cros.factory.test import state
+  global _state_instance # pylint: disable=W0603
+  if _state_instance is None:
+    _state_instance = state.get_instance()
+  return _state_instance
 
 
 def get_shared_data(key, default=None):
-    if not get_state_instance().has_shared_data(key):
-        return default
-    return get_state_instance().get_shared_data(key)
+  if not get_state_instance().has_shared_data(key):
+    return default
+  return get_state_instance().get_shared_data(key)
 
 
 def set_shared_data(*key_value_pairs):
-    return get_state_instance().set_shared_data(*key_value_pairs)
+  return get_state_instance().set_shared_data(*key_value_pairs)
 
 
 def has_shared_data(key):
-    return get_state_instance().has_shared_data(key)
+  return get_state_instance().has_shared_data(key)
 
 
 def del_shared_data(key):
-    return get_state_instance().del_shared_data(key)
+  return get_state_instance().del_shared_data(key)
 
 
 def read_test_list(path=None, state_instance=None, text=None,
-                   test_classes={}):
-    if len([x for x in [path, text] if x]) != 1:
-        raise TestListError('Exactly one of path and text must be set')
+          test_classes={}):
+  if len([x for x in [path, text] if x]) != 1:
+    raise TestListError('Exactly one of path and text must be set')
 
-    test_list_locals = {}
+  test_list_locals = {}
 
-    # Import test classes into the evaluation namespace
-    for d in dict(globals()), test_classes:
-        for (k, v) in d.iteritems():
-            if type(v) == type and issubclass(v, FactoryTest):
-                test_list_locals[k] = v
+  # Import test classes into the evaluation namespace
+  for d in dict(globals()), test_classes:
+    for (k, v) in d.iteritems():
+      if type(v) == type and issubclass(v, FactoryTest):
+        test_list_locals[k] = v
 
-    # Import WLAN into the evaluation namespace, since it is used
-    # to construct the wlans option.
-    test_list_locals['WLAN'] = connection_manager.WLAN
+  # Import WLAN into the evaluation namespace, since it is used
+  # to construct the wlans option.
+  test_list_locals['WLAN'] = connection_manager.WLAN
 
-    options = Options()
-    test_list_locals['options'] = options
+  options = Options()
+  test_list_locals['options'] = options
 
-    if path:
-        execfile(path, {}, test_list_locals)
-    else:
-        exec text in {}, test_list_locals
-    assert 'TEST_LIST' in test_list_locals, (
-        'Test list %s does not define TEST_LIST' % (path or '<text>'))
+  if path:
+    execfile(path, {}, test_list_locals)
+  else:
+    exec text in {}, test_list_locals
+  assert 'TEST_LIST' in test_list_locals, (
+    'Test list %s does not define TEST_LIST' % (path or '<text>'))
 
-    options.check_valid()
+  options.check_valid()
 
-    return FactoryTestList(test_list_locals['TEST_LIST'],
-                           state_instance or get_state_instance(),
-                           options)
+  return FactoryTestList(test_list_locals['TEST_LIST'],
+              state_instance or get_state_instance(),
+              options)
 
 
 _inited_logging = False
 def init_logging(prefix=None, verbose=False):
-    '''
-    Initializes logging.
+  '''
+  Initializes logging.
 
-    @param prefix: A prefix to display for each log line, e.g., the program
-        name.
-    @param verbose: True for debug logging, false for info logging.
-    '''
-    global _inited_logging  # pylint: disable=W0603
-    assert not _inited_logging, "May only call init_logging once"
-    _inited_logging = True
+  @param prefix: A prefix to display for each log line, e.g., the program
+    name.
+  @param verbose: True for debug logging, false for info logging.
+  '''
+  global _inited_logging # pylint: disable=W0603
+  assert not _inited_logging, "May only call init_logging once"
+  _inited_logging = True
 
-    if not prefix:
-        prefix = os.path.basename(sys.argv[0])
+  if not prefix:
+    prefix = os.path.basename(sys.argv[0])
 
-    # Make sure that nothing else has initialized logging yet (e.g.,
-    # autotest, whose logging_config does basicConfig).
-    assert not logging.getLogger().handlers, (
-        "Logging has already been initialized")
+  # Make sure that nothing else has initialized logging yet (e.g.,
+  # autotest, whose logging_config does basicConfig).
+  assert not logging.getLogger().handlers, (
+    "Logging has already been initialized")
 
-    logging.basicConfig(
-        format=('[%(levelname)s] ' + prefix +
-                ' %(filename)s:%(lineno)d %(asctime)s.%(msecs)03d %(message)s'),
-        level=logging.DEBUG if verbose else logging.INFO,
-        datefmt='%Y-%m-%d %H:%M:%S')
+  logging.basicConfig(
+    format=('[%(levelname)s] ' + prefix +
+        ' %(filename)s:%(lineno)d %(asctime)s.%(msecs)03d %(message)s'),
+    level=logging.DEBUG if verbose else logging.INFO,
+    datefmt='%Y-%m-%d %H:%M:%S')
 
-    logging.debug('Initialized logging')
+  logging.debug('Initialized logging')
 
 
 class Options(object):
-    '''Test list options.
+  '''Test list options.
 
-    These may be set by assigning to the options variable in a test list (e.g.,
-    'options.auto_run_on_start = False').
-    '''
-    # Allowable types for an option (defaults to the type of the default
-    # value).
-    _types = {}
+  These may be set by assigning to the options variable in a test list (e.g.,
+  'options.auto_run_on_start = False').
+  '''
+  # Allowable types for an option (defaults to the type of the default
+  # value).
+  _types = {}
 
-    # Perform an implicit auto-run when the test driver starts up?
-    auto_run_on_start = True
+  # Perform an implicit auto-run when the test driver starts up?
+  auto_run_on_start = True
 
-    # Perform an implicit auto-run when the user switches to any test?
-    auto_run_on_keypress = False
+  # Perform an implicit auto-run when the user switches to any test?
+  auto_run_on_keypress = False
 
-    # Default UI language
-    ui_lang = 'en'
+  # Default UI language
+  ui_lang = 'en'
 
-    # Preserve only autotest results matching these globs.
-    preserve_autotest_results = ['*.DEBUG', '*.INFO']
+  # Preserve only autotest results matching these globs.
+  preserve_autotest_results = ['*.DEBUG', '*.INFO']
 
-    # Maximum amount of time allowed between reboots.  If this threshold is
-    # exceeded, the reboot is considered failed.
-    max_reboot_time_secs = 180
+  # Maximum amount of time allowed between reboots. If this threshold is
+  # exceeded, the reboot is considered failed.
+  max_reboot_time_secs = 180
 
-    # SHA1 hash for a eng password in UI.  Use None to always
-    # enable eng mode.  To generate, run `echo -n '<password>'
-    # | sha1sum`.
-    engineering_password_sha1 = None
-    _types['engineering_password_sha1'] = (type(None), str)
+  # SHA1 hash for a eng password in UI. Use None to always
+  # enable eng mode. To generate, run `echo -n '<password>'
+  # | sha1sum`.
+  engineering_password_sha1 = None
+  _types['engineering_password_sha1'] = (type(None), str)
 
-    # WLANs that the connection manager may connect to.
-    wlans = []
+  # WLANs that the connection manager may connect to.
+  wlans = []
 
-    # Automatically send events to the shopfloor server when
-    # it is reachable.
-    sync_event_log_period_secs = None
-    _types['sync_event_log_period_secs'] = (type(None), int)
+  # Automatically send events to the shopfloor server when
+  # it is reachable.
+  sync_event_log_period_secs = None
+  _types['sync_event_log_period_secs'] = (type(None), int)
 
-    # Timeout talking to shopfloor server for background operations.
-    shopfloor_timeout_secs = 10
+  # Timeout talking to shopfloor server for background operations.
+  shopfloor_timeout_secs = 10
 
-    def check_valid(self):
-        '''Throws a TestListError if there are any invalid options.'''
-        # Make sure no errant options, or options with weird types,
-        # were set.
-        default_options = Options()
-        for key in sorted(self.__dict__):
-            if key.startswith('_'):
-                continue
-            if not hasattr(default_options, key):
-                raise TestListError('Unknown option %s' % key)
+  def check_valid(self):
+    '''Throws a TestListError if there are any invalid options.'''
+    # Make sure no errant options, or options with weird types,
+    # were set.
+    default_options = Options()
+    for key in sorted(self.__dict__):
+      if key.startswith('_'):
+        continue
+      if not hasattr(default_options, key):
+        raise TestListError('Unknown option %s' % key)
 
-            value = getattr(self, key)
-            allowable_types = Options._types.get(
-                key,
-                [type(getattr(default_options, key))]);
-            if type(value) not in allowable_types:
-                raise TestListError(
-                    'Option %s has unexpected type %s (should be %s)' % (
-                        key, type(value), allowable_types))
+      value = getattr(self, key)
+      allowable_types = Options._types.get(
+        key,
+        [type(getattr(default_options, key))]);
+      if type(value) not in allowable_types:
+        raise TestListError(
+          'Option %s has unexpected type %s (should be %s)' % (
+            key, type(value), allowable_types))
 
 
 class TestState(object):
+  '''
+  The complete state of a test.
+
+  @property status: The status of the test (one of ACTIVE, PASSED,
+    FAILED, or UNTESTED).
+  @property count: The number of times the test has been run.
+  @property error_msg: The last error message that caused a test failure.
+  @property shutdown_count: The next of times the test has caused a shutdown.
+  @property visible: Whether the test is the currently visible test.
+  @property invocation: The currently executing invocation.
+  '''
+  ACTIVE = 'ACTIVE'
+  PASSED = 'PASSED'
+  FAILED = 'FAILED'
+  UNTESTED = 'UNTESTED'
+
+  def __init__(self, status=UNTESTED, count=0, visible=False, error_msg=None,
+         shutdown_count=0, invocation=None):
+    self.status = status
+    self.count = count
+    self.visible = visible
+    self.error_msg = error_msg
+    self.shutdown_count = shutdown_count
+    self.invocation = None
+
+  def __repr__(self):
+    return std_repr(self)
+
+  def update(self, status=None, increment_count=0, error_msg=None,
+        shutdown_count=None, increment_shutdown_count=0, visible=None,
+        invocation=None):
     '''
-    The complete state of a test.
+    Updates the state of a test.
 
-    @property status: The status of the test (one of ACTIVE, PASSED,
-        FAILED, or UNTESTED).
-    @property count: The number of times the test has been run.
-    @property error_msg: The last error message that caused a test failure.
-    @property shutdown_count: The next of times the test has caused a shutdown.
-    @property visible: Whether the test is the currently visible test.
-    @property invocation: The currently executing invocation.
+    @param status: The new status of the test.
+    @param increment_count: An amount by which to increment count.
+    @param error_msg: If non-None, the new error message for the test.
+    @param shutdown_count: If non-None, the new shutdown count.
+    @param increment_shutdown_count: An amount by which to increment
+      shutdown_count.
+    @param visible: If non-None, whether the test should become visible.
+    @param invocation: The currently executing invocation, if any.
+      (Applies only if the status is ACTIVE.)
+
+    Returns True if anything was changed.
     '''
-    ACTIVE = 'ACTIVE'
-    PASSED = 'PASSED'
-    FAILED = 'FAILED'
-    UNTESTED = 'UNTESTED'
+    old_dict = dict(self.__dict__)
 
-    def __init__(self, status=UNTESTED, count=0, visible=False, error_msg=None,
-                 shutdown_count=0, invocation=None):
-        self.status = status
-        self.count = count
-        self.visible = visible
-        self.error_msg = error_msg
-        self.shutdown_count = shutdown_count
-        self.invocation = None
+    if status:
+      self.status = status
+    if error_msg is not None:
+      self.error_msg = error_msg
+    if shutdown_count is not None:
+      self.shutdown_count = shutdown_count
+    if visible is not None:
+      self.visible = visible
 
-    def __repr__(self):
-        return std_repr(self)
+    if self.status != self.ACTIVE:
+      self.invocation = None
+    elif invocation is not None:
+      self.invocation = invocation
 
-    def update(self, status=None, increment_count=0, error_msg=None,
-               shutdown_count=None, increment_shutdown_count=0, visible=None,
-               invocation=None):
-        '''
-        Updates the state of a test.
+    self.count += increment_count
+    self.shutdown_count += increment_shutdown_count
 
-        @param status: The new status of the test.
-        @param increment_count: An amount by which to increment count.
-        @param error_msg: If non-None, the new error message for the test.
-        @param shutdown_count: If non-None, the new shutdown count.
-        @param increment_shutdown_count: An amount by which to increment
-            shutdown_count.
-        @param visible: If non-None, whether the test should become visible.
-        @param invocation: The currently executing invocation, if any.
-            (Applies only if the status is ACTIVE.)
+    return self.__dict__ != old_dict
 
-        Returns True if anything was changed.
-        '''
-        old_dict = dict(self.__dict__)
-
-        if status:
-            self.status = status
-        if error_msg is not None:
-            self.error_msg = error_msg
-        if shutdown_count is not None:
-            self.shutdown_count = shutdown_count
-        if visible is not None:
-            self.visible = visible
-
-        if self.status != self.ACTIVE:
-            self.invocation = None
-        elif invocation is not None:
-            self.invocation = invocation
-
-        self.count += increment_count
-        self.shutdown_count += increment_shutdown_count
-
-        return self.__dict__ != old_dict
-
-    @classmethod
-    def from_dict_or_object(cls, obj):
-        if type(obj) == dict:
-            return TestState(**obj)
-        else:
-            assert type(obj) == TestState, type(obj)
-            return obj
+  @classmethod
+  def from_dict_or_object(cls, obj):
+    if type(obj) == dict:
+      return TestState(**obj)
+    else:
+      assert type(obj) == TestState, type(obj)
+      return obj
 
 
 class FactoryTest(object):
+  '''
+  A factory test object.
+
+  Factory tests are stored in a tree. Each node has an id (unique
+  among its siblings). Each node also has a path (unique throughout the
+  tree), constructed by joining the IDs of all the test's ancestors
+  with a '.' delimiter.
+  '''
+
+  # If True, the test never fails, but only returns to an untested state.
+  never_fails = False
+
+  # If True, the test has a UI, so if it is active factory_ui will not
+  # display the summary of running tests.
+  has_ui = False
+
+  REPR_FIELDS = ['id', 'autotest_name', 'pytest_name', 'dargs',
+          'backgroundable', 'exclusive', 'never_fails']
+
+  # Subsystems that the test may require exclusive access to.
+  EXCLUSIVE_OPTIONS = utils.Enum(['NETWORKING'])
+
+  def __init__(self,
+         label_en='',
+         label_zh='',
+         autotest_name=None,
+         pytest_name=None,
+         invocation_target=None,
+         kbd_shortcut=None,
+         dargs=None,
+         backgroundable=False,
+         subtests=None,
+         id=None,         # pylint: disable=W0622
+         has_ui=None,
+         never_fails=None,
+         exclusive=None,
+         _root=None,
+         _default_id=None):
     '''
-    A factory test object.
+    Constructor.
 
-    Factory tests are stored in a tree.  Each node has an id (unique
-    among its siblings).  Each node also has a path (unique throughout the
-    tree), constructed by joining the IDs of all the test's ancestors
-    with a '.' delimiter.
+    @param label_en: An English label.
+    @param label_zh: A Chinese label.
+    @param autotest_name: The name of the autotest to run.
+    @param pytest_name: The name of the pytest to run (relative to
+      autotest_lib.client.cros.factory.tests).
+    @param invocation_target: The function to execute to run the test
+      (within the Goofy process).
+    @param kbd_shortcut: The keyboard shortcut for the test.
+    @param dargs: Autotest arguments.
+    @param backgroundable: Whether the test may run in the background.
+    @param subtests: A list of tests to run inside this test.
+    @param id: A unique ID for the test (defaults to the autotest name).
+    @param has_ui: True if the test has a UI. (This defaults to True for
+      OperatorTest.) If has_ui is not True, then when the test is
+      running, the statuses of the test and its siblings will be shown in
+      the test UI area instead.
+    @param never_fails: True if the test never fails, but only returns to an
+      untested state.
+    @param exclusive: Items that the test may require exclusive access to.
+      May be a list or a single string. Items must all be in
+      EXCLUSIVE_OPTIONS. Tests may not be backgroundable.
+    @param _default_id: A default ID to use if no ID is specified.
+    @param _root: True only if this is the root node (for internal use
+      only).
     '''
+    self.label_en = label_en
+    self.label_zh = label_zh
+    self.autotest_name = autotest_name
+    self.pytest_name = pytest_name
+    self.invocation_target = invocation_target
+    self.kbd_shortcut = kbd_shortcut.lower() if kbd_shortcut else None
+    self.dargs = dargs or {}
+    self.backgroundable = backgroundable
+    if isinstance(exclusive, str):
+      self.exclusive = [exclusive]
+    else:
+      self.exclusive = exclusive or []
+    self.subtests = subtests or []
+    self.path = ''
+    self.parent = None
+    self.root = None
 
-    # If True, the test never fails, but only returns to an untested state.
-    never_fails = False
+    if _root:
+      self.id = None
+    else:
+      if id:
+        self.id = id
+      elif autotest_name:
+        self.id = autotest_name
+      elif pytest_name:
+        self.id = pytest_name.rpartition('.')[2]
+      else:
+        self.id = _default_id
 
-    # If True, the test has a UI, so if it is active factory_ui will not
-    # display the summary of running tests.
-    has_ui = False
+      assert self.id, (
+        'id not specified for test: %r' % self)
+      assert '.' not in self.id, (
+        'id cannot contain a period: %r' % self)
+      # Note that we check ID uniqueness in _init.
 
-    REPR_FIELDS = ['id', 'autotest_name', 'pytest_name', 'dargs',
-                   'backgroundable', 'exclusive', 'never_fails']
+    assert len(filter(None, [autotest_name, pytest_name,
+                 invocation_target, subtests])) <= 1, (
+      'No more than one of autotest_name, pytest_name, '
+      'invocation_target, and subtests must be specified')
 
-    # Subsystems that the test may require exclusive access to.
-    EXCLUSIVE_OPTIONS = utils.Enum(['NETWORKING'])
+    if has_ui is not None:
+      self.has_ui = has_ui
+    if never_fails is not None:
+      self.never_fails = never_fails
 
-    def __init__(self,
-                 label_en='',
-                 label_zh='',
-                 autotest_name=None,
-                 pytest_name=None,
-                 invocation_target=None,
-                 kbd_shortcut=None,
-                 dargs=None,
-                 backgroundable=False,
-                 subtests=None,
-                 id=None,                  # pylint: disable=W0622
-                 has_ui=None,
-                 never_fails=None,
-                 exclusive=None,
-                 _root=None,
-                 _default_id=None):
-        '''
-        Constructor.
+    # Auto-assign label text.
+    if not self.label_en:
+      if self.id and (self.id != self.autotest_name):
+        self.label_en = self.id
+      elif self.autotest_name:
+        # autotest_name is type_NameInCamelCase.
+        self.label_en = self.autotest_name.partition('_')[2]
 
-        @param label_en: An English label.
-        @param label_zh: A Chinese label.
-        @param autotest_name: The name of the autotest to run.
-        @param pytest_name: The name of the pytest to run (relative to
-            autotest_lib.client.cros.factory.tests).
-        @param invocation_target: The function to execute to run the test
-            (within the Goofy process).
-        @param kbd_shortcut: The keyboard shortcut for the test.
-        @param dargs: Autotest arguments.
-        @param backgroundable: Whether the test may run in the background.
-        @param subtests: A list of tests to run inside this test.
-        @param id: A unique ID for the test (defaults to the autotest name).
-        @param has_ui: True if the test has a UI.  (This defaults to True for
-            OperatorTest.)  If has_ui is not True, then when the test is
-            running, the statuses of the test and its siblings will be shown in
-            the test UI area instead.
-        @param never_fails: True if the test never fails, but only returns to an
-            untested state.
-        @param exclusive: Items that the test may require exclusive access to.
-            May be a list or a single string.  Items must all be in
-            EXCLUSIVE_OPTIONS.  Tests may not be backgroundable.
-        @param _default_id: A default ID to use if no ID is specified.
-        @param _root: True only if this is the root node (for internal use
-            only).
-        '''
-        self.label_en = label_en
-        self.label_zh = label_zh
-        self.autotest_name = autotest_name
-        self.pytest_name = pytest_name
-        self.invocation_target = invocation_target
-        self.kbd_shortcut = kbd_shortcut.lower() if kbd_shortcut else None
-        self.dargs = dargs or {}
-        self.backgroundable = backgroundable
-        if isinstance(exclusive, str):
-            self.exclusive = [exclusive]
-        else:
-            self.exclusive = exclusive or []
-        self.subtests = subtests or []
-        self.path = ''
-        self.parent = None
-        self.root = None
+    assert not (backgroundable and exclusive), (
+      'Test %s may not have both backgroundable and exclusive' %
+      self.id)
+    bogus_exclusive_items = set(self.exclusive) - self.EXCLUSIVE_OPTIONS
+    assert not bogus_exclusive_items, (
+      'In test %s, invalid exclusive options: %s (should be in %s)' % (
+        self.id,
+        bogus_exclusive_items,
+        self.EXCLUSIVE_OPTIONS))
 
-        if _root:
-            self.id = None
-        else:
-            if id:
-                self.id = id
-            elif autotest_name:
-                self.id = autotest_name
-            elif pytest_name:
-                self.id = pytest_name.rpartition('.')[2]
-            else:
-                self.id = _default_id
-
-            assert self.id, (
-                'id not specified for test: %r' % self)
-            assert '.' not in self.id, (
-                'id cannot contain a period: %r' % self)
-            # Note that we check ID uniqueness in _init.
-
-        assert len(filter(None, [autotest_name, pytest_name,
-                                 invocation_target, subtests])) <= 1, (
-            'No more than one of autotest_name, pytest_name, '
-            'invocation_target, and subtests must be specified')
-
-        if has_ui is not None:
-            self.has_ui = has_ui
-        if never_fails is not None:
-            self.never_fails = never_fails
-
-        # Auto-assign label text.
-        if not self.label_en:
-            if self.id and (self.id != self.autotest_name):
-                self.label_en = self.id
-            elif self.autotest_name:
-                # autotest_name is type_NameInCamelCase.
-                self.label_en = self.autotest_name.partition('_')[2]
-
-        assert not (backgroundable and exclusive), (
-            'Test %s may not have both backgroundable and exclusive' %
-            self.id)
-        bogus_exclusive_items = set(self.exclusive) - self.EXCLUSIVE_OPTIONS
-        assert not bogus_exclusive_items, (
-            'In test %s, invalid exclusive options: %s (should be in %s)' % (
-                self.id,
-                bogus_exclusive_items,
-                self.EXCLUSIVE_OPTIONS))
-
-    def to_struct(self):
-        '''Returns the node as a struct suitable for JSONification.'''
-        ret = dict(
-            (k, getattr(self, k))
-            for k in ['id', 'path', 'label_en', 'label_zh',
-                      'kbd_shortcut', 'backgroundable'])
-        ret['subtests'] = [subtest.to_struct() for subtest in self.subtests]
-        return ret
+  def to_struct(self):
+    '''Returns the node as a struct suitable for JSONification.'''
+    ret = dict(
+      (k, getattr(self, k))
+      for k in ['id', 'path', 'label_en', 'label_zh',
+           'kbd_shortcut', 'backgroundable'])
+    ret['subtests'] = [subtest.to_struct() for subtest in self.subtests]
+    return ret
 
 
-    def __repr__(self, recursive=False):
-        attrs = ['%s=%s' % (k, repr(getattr(self, k)))
-                 for k in sorted(self.__dict__.keys())
-                 if k in FactoryTest.REPR_FIELDS and getattr(self, k)]
-        if recursive and self.subtests:
-            indent = '    ' * (1 + self.path.count('.'))
-            attrs.append(
-                'subtests=[' +
-                ('\n' +
-                 ',\n'.join([subtest.__repr__(recursive)
-                             for subtest in self.subtests]
-                            )).replace('\n', '\n' + indent)
-                + '\n]')
+  def __repr__(self, recursive=False):
+    attrs = ['%s=%s' % (k, repr(getattr(self, k)))
+         for k in sorted(self.__dict__.keys())
+         if k in FactoryTest.REPR_FIELDS and getattr(self, k)]
+    if recursive and self.subtests:
+      indent = '  ' * (1 + self.path.count('.'))
+      attrs.append(
+        'subtests=[' +
+        ('\n' +
+         ',\n'.join([subtest.__repr__(recursive)
+               for subtest in self.subtests]
+              )).replace('\n', '\n' + indent)
+        + '\n]')
 
-        return '%s(%s)' % (self.__class__.__name__, ', '.join(attrs))
+    return '%s(%s)' % (self.__class__.__name__, ', '.join(attrs))
 
-    def _init(self, prefix, path_map):
-        '''
-        Recursively assigns paths to this node and its children.
+  def _init(self, prefix, path_map):
+    '''
+    Recursively assigns paths to this node and its children.
 
-        Also adds this node to the root's path_map.
-        '''
-        if self.parent:
-            self.root = self.parent.root
+    Also adds this node to the root's path_map.
+    '''
+    if self.parent:
+      self.root = self.parent.root
 
-        self.path = prefix + (self.id or '')
-        assert self.path not in path_map, 'Duplicate test path %s' % (self.path)
-        path_map[self.path] = self
+    self.path = prefix + (self.id or '')
+    assert self.path not in path_map, 'Duplicate test path %s' % (self.path)
+    path_map[self.path] = self
 
-        for subtest in self.subtests:
-            subtest.parent = self
-            subtest._init((self.path + '.' if len(self.path) else ''), path_map)
+    for subtest in self.subtests:
+      subtest.parent = self
+      subtest._init((self.path + '.' if len(self.path) else ''), path_map)
 
-    def depth(self):
-        '''
-        Returns the depth of the node (0 for the root).
-        '''
-        return self.path.count('.') + (self.parent is not None)
+  def depth(self):
+    '''
+    Returns the depth of the node (0 for the root).
+    '''
+    return self.path.count('.') + (self.parent is not None)
 
-    def is_leaf(self):
-        '''
-        Returns true if this is a leaf node.
-        '''
-        return not self.subtests
+  def is_leaf(self):
+    '''
+    Returns true if this is a leaf node.
+    '''
+    return not self.subtests
 
-    def get_ancestors(self):
-      '''
-      Returns list of ancestors, ordered by seniority.
-      '''
-      if self.parent is not None:
-        return self.parent.get_ancestors() + [self.parent]
-      return []
+  def get_ancestors(self):
+   '''
+   Returns list of ancestors, ordered by seniority.
+   '''
+   if self.parent is not None:
+    return self.parent.get_ancestors() + [self.parent]
+   return []
 
-    def get_ancestor_groups(self):
-      '''
-      Returns list of ancestors that are groups, ordered by seniority.
-      '''
-      return [node for node in self.get_ancestors() if node.is_group()]
+  def get_ancestor_groups(self):
+   '''
+   Returns list of ancestors that are groups, ordered by seniority.
+   '''
+   return [node for node in self.get_ancestors() if node.is_group()]
 
-    def get_state(self):
-        '''
-        Returns the current test state from the state instance.
-        '''
-        return TestState.from_dict_or_object(
-            self.root.state_instance.get_test_state(self.path))
+  def get_state(self):
+    '''
+    Returns the current test state from the state instance.
+    '''
+    return TestState.from_dict_or_object(
+      self.root.state_instance.get_test_state(self.path))
 
-    def update_state(self, update_parent=True, status=None, **kw):
-        '''
-        Updates the test state.
+  def update_state(self, update_parent=True, status=None, **kw):
+    '''
+    Updates the test state.
 
-        See TestState.update for allowable kw arguments.
-        '''
-        if self.never_fails and status == TestState.FAILED:
-            status = TestState.UNTESTED
+    See TestState.update for allowable kw arguments.
+    '''
+    if self.never_fails and status == TestState.FAILED:
+      status = TestState.UNTESTED
 
-        ret = TestState.from_dict_or_object(
-            self.root._update_test_state(  # pylint: disable=W0212
-                self.path, status=status, **kw))
-        if update_parent and self.parent:
-            self.parent.update_status_from_children()
-        return ret
+    ret = TestState.from_dict_or_object(
+      self.root._update_test_state( # pylint: disable=W0212
+        self.path, status=status, **kw))
+    if update_parent and self.parent:
+      self.parent.update_status_from_children()
+    return ret
 
-    def update_status_from_children(self):
-        '''
-        Updates the status based on children's status.
+  def update_status_from_children(self):
+    '''
+    Updates the status based on children's status.
 
-        A test is active if any children are active; else failed if
-        any children are failed; else untested if any children are
-        untested; else passed.
-        '''
-        if not self.subtests:
-            return
+    A test is active if any children are active; else failed if
+    any children are failed; else untested if any children are
+    untested; else passed.
+    '''
+    if not self.subtests:
+      return
 
-        statuses = set([x.get_state().status for x in self.subtests])
+    statuses = set([x.get_state().status for x in self.subtests])
 
-        # If there are any active tests, consider it active; if any failed,
-        # consider it failed, etc.  The order is important!
-        # pylint: disable=W0631
-        for status in [TestState.ACTIVE, TestState.FAILED,
-                       TestState.UNTESTED, TestState.PASSED]:
-            if status in statuses:
-                break
+    # If there are any active tests, consider it active; if any failed,
+    # consider it failed, etc. The order is important!
+    # pylint: disable=W0631
+    for status in [TestState.ACTIVE, TestState.FAILED,
+            TestState.UNTESTED, TestState.PASSED]:
+      if status in statuses:
+        break
 
-        if status != self.get_state().status:
-            self.update_state(status=status)
+    if status != self.get_state().status:
+      self.update_state(status=status)
 
-    def walk(self, in_order=False):
-        '''
-        Yields this test and each sub-test.
+  def walk(self, in_order=False):
+    '''
+    Yields this test and each sub-test.
 
-        @param in_order: Whether to walk in-order.  If False, walks depth-first.
-        '''
-        if in_order:
-            # Walking in order - yield self first.
-            yield self
-        for subtest in self.subtests:
-            for f in subtest.walk(in_order):
-                yield f
-        if not in_order:
-            # Walking depth first - yield self last.
-            yield self
+    @param in_order: Whether to walk in-order. If False, walks depth-first.
+    '''
+    if in_order:
+      # Walking in order - yield self first.
+      yield self
+    for subtest in self.subtests:
+      for f in subtest.walk(in_order):
+        yield f
+    if not in_order:
+      # Walking depth first - yield self last.
+      yield self
 
-    def is_group(self):
-        '''
-        Returns true if this node is a test group.
-        '''
-        return isinstance(self, TestGroup)
+  def is_group(self):
+    '''
+    Returns true if this node is a test group.
+    '''
+    return isinstance(self, TestGroup)
 
-    def is_top_level_test(self):
-        '''
-        Returns true if this node is a top-level test.
+  def is_top_level_test(self):
+    '''
+    Returns true if this node is a top-level test.
 
-        A 'top-level test' is a test directly underneath the root or a
-        TestGroup, e.g., a node under which all tests must be run
-        together to be meaningful.
-        '''
-        return ((not self.is_group()) and
-                self.parent and
-                (self.parent == self.root or self.parent.is_group()))
+    A 'top-level test' is a test directly underneath the root or a
+    TestGroup, e.g., a node under which all tests must be run
+    together to be meaningful.
+    '''
+    return ((not self.is_group()) and
+        self.parent and
+        (self.parent == self.root or self.parent.is_group()))
 
-    def get_top_level_parent_or_group(self):
-        if self.is_group() or self.is_top_level_test() or not self.parent:
-            return self
-        return self.parent.get_top_level_parent_or_group()
+  def get_top_level_parent_or_group(self):
+    if self.is_group() or self.is_top_level_test() or not self.parent:
+      return self
+    return self.parent.get_top_level_parent_or_group()
 
-    def get_top_level_tests(self):
-        '''
-        Returns a list of top-level tests.
-        '''
-        return [node for node in self.walk()
-                if node.is_top_level_test()]
+  def get_top_level_tests(self):
+    '''
+    Returns a list of top-level tests.
+    '''
+    return [node for node in self.walk()
+        if node.is_top_level_test()]
 
-    def is_exclusive(self, option):
-        '''
-        Returns true if the test or any parent is exclusive w.r.t. option.
+  def is_exclusive(self, option):
+    '''
+    Returns true if the test or any parent is exclusive w.r.t. option.
 
-        Args:
-          option: A member of EXCLUSIVE_OPTIONS.
-        '''
-        assert option in self.EXCLUSIVE_OPTIONS
-        return option in self.exclusive or (
-            self.parent and self.parent.is_exclusive(option))
+    Args:
+     option: A member of EXCLUSIVE_OPTIONS.
+    '''
+    assert option in self.EXCLUSIVE_OPTIONS
+    return option in self.exclusive or (
+      self.parent and self.parent.is_exclusive(option))
 
 
 class FactoryTestList(FactoryTest):
+  '''
+  The root node for factory tests.
+
+  Properties:
+    path_map: A map from test paths to FactoryTest objects.
+  '''
+  def __init__(self, subtests, state_instance, options):
+    super(FactoryTestList, self).__init__(_root=True, subtests=subtests)
+    self.state_instance = state_instance
+    self.subtests = subtests
+    self.path_map = {}
+    self.root = self
+    self.state_change_callback = None
+    self.options = options
+    self._init('', self.path_map)
+
+  def get_all_tests(self):
     '''
-    The root node for factory tests.
-
-    Properties:
-        path_map: A map from test paths to FactoryTest objects.
+    Returns all FactoryTest objects.
     '''
-    def __init__(self, subtests, state_instance, options):
-        super(FactoryTestList, self).__init__(_root=True, subtests=subtests)
-        self.state_instance = state_instance
-        self.subtests = subtests
-        self.path_map = {}
-        self.root = self
-        self.state_change_callback = None
-        self.options = options
-        self._init('', self.path_map)
+    return self.path_map.values()
 
-    def get_all_tests(self):
-        '''
-        Returns all FactoryTest objects.
-        '''
-        return self.path_map.values()
+  def get_state_map(self):
+    '''
+    Returns a map of all FactoryTest objects to their TestStates.
+    '''
+    # The state instance may return a dict (for the XML/RPC proxy)
+    # or the TestState object itself. Convert accordingly.
+    return dict(
+      (self.lookup_path(k), TestState.from_dict_or_object(v))
+      for k, v in self.state_instance.get_test_states().iteritems())
 
-    def get_state_map(self):
-        '''
-        Returns a map of all FactoryTest objects to their TestStates.
-        '''
-        # The state instance may return a dict (for the XML/RPC proxy)
-        # or the TestState object itself.  Convert accordingly.
-        return dict(
-            (self.lookup_path(k), TestState.from_dict_or_object(v))
-            for k, v in self.state_instance.get_test_states().iteritems())
+  def lookup_path(self, path):
+    '''
+    Looks up a test from its path.
+    '''
+    return self.path_map.get(path, None)
 
-    def lookup_path(self, path):
-        '''
-        Looks up a test from its path.
-        '''
-        return self.path_map.get(path, None)
+  def _update_test_state(self, path, **kw):
+    '''
+    Updates a test state, invoking the state_change_callback if any.
 
-    def _update_test_state(self, path, **kw):
-        '''
-        Updates a test state, invoking the state_change_callback if any.
-
-        Internal-only; clients should call update_state directly on the
-        appropriate TestState object.
-        '''
-        ret, changed = self.state_instance.update_test_state(path, **kw)
-        if changed and self.state_change_callback:
-            self.state_change_callback(  # pylint: disable=E1102
-                self.lookup_path(path), ret)
-        return ret
+    Internal-only; clients should call update_state directly on the
+    appropriate TestState object.
+    '''
+    ret, changed = self.state_instance.update_test_state(path, **kw)
+    if changed and self.state_change_callback:
+      self.state_change_callback( # pylint: disable=E1102
+        self.lookup_path(path), ret)
+    return ret
 
 
 class TestGroup(FactoryTest):
-    '''
-    A collection of related tests, shown together in RHS panel if one is active.
-    '''
-    pass
+  '''
+  A collection of related tests, shown together in RHS panel if one is active.
+  '''
+  pass
 
 
 class FactoryAutotestTest(FactoryTest):
-    pass
+  pass
 
 
 class OperatorTest(FactoryAutotestTest):
-    has_ui = True
+  has_ui = True
 
 
 AutomatedSequence = FactoryTest
@@ -772,44 +772,44 @@
 
 
 class ShutdownStep(AutomatedSubTest):
-    '''A shutdown (halt or reboot) step.
+  '''A shutdown (halt or reboot) step.
 
-    Properties:
-        iterations: The number of times to reboot.
-        operation: The command to run to perform the shutdown
-            (REBOOT or HALT).
-        delay_secs: Number of seconds the operator has to abort the shutdown.
-    '''
-    REBOOT = 'reboot'
-    HALT = 'halt'
+  Properties:
+    iterations: The number of times to reboot.
+    operation: The command to run to perform the shutdown
+      (REBOOT or HALT).
+    delay_secs: Number of seconds the operator has to abort the shutdown.
+  '''
+  REBOOT = 'reboot'
+  HALT = 'halt'
 
-    def __init__(self, operation, iterations=1, delay_secs=5, **kw):
-        kw.setdefault('id', operation)
-        super(ShutdownStep, self).__init__(**kw)
-        assert not self.autotest_name, (
-            'Reboot/halt steps may not have an autotest')
-        assert not self.subtests, 'Reboot/halt steps may not have subtests'
-        assert not self.backgroundable, (
-            'Reboot/halt steps may not be backgroundable')
+  def __init__(self, operation, iterations=1, delay_secs=5, **kw):
+    kw.setdefault('id', operation)
+    super(ShutdownStep, self).__init__(**kw)
+    assert not self.autotest_name, (
+      'Reboot/halt steps may not have an autotest')
+    assert not self.subtests, 'Reboot/halt steps may not have subtests'
+    assert not self.backgroundable, (
+      'Reboot/halt steps may not be backgroundable')
 
-        assert iterations > 0
-        self.iterations = iterations
-        assert operation in [self.REBOOT, self.HALT]
-        self.operation = operation
-        assert delay_secs >= 0
-        self.delay_secs = delay_secs
+    assert iterations > 0
+    self.iterations = iterations
+    assert operation in [self.REBOOT, self.HALT]
+    self.operation = operation
+    assert delay_secs >= 0
+    self.delay_secs = delay_secs
 
 
 class HaltStep(ShutdownStep):
-    '''Halts the machine.'''
-    def __init__(self, **kw):
-        super(HaltStep, self).__init__(operation=ShutdownStep.HALT, **kw)
+  '''Halts the machine.'''
+  def __init__(self, **kw):
+    super(HaltStep, self).__init__(operation=ShutdownStep.HALT, **kw)
 
 
 class RebootStep(ShutdownStep):
-    '''Reboots the machine.'''
-    def __init__(self, **kw):
-        super(RebootStep, self).__init__(operation=ShutdownStep.REBOOT, **kw)
+  '''Reboots the machine.'''
+  def __init__(self, **kw):
+    super(RebootStep, self).__init__(operation=ShutdownStep.REBOOT, **kw)
 
 
 AutomatedRebootSubTest = RebootStep
diff --git a/py/test/factory_unittest.py b/py/test/factory_unittest.py
index a29a5e2..5973067 100755
--- a/py/test/factory_unittest.py
+++ b/py/test/factory_unittest.py
@@ -6,7 +6,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import factory_common
+import factory_common # pylint: disable=W0611
 
 import glob
 import logging
@@ -21,62 +21,62 @@
 
 
 class FactoryTest(unittest.TestCase):
-    def test_parse_test_lists(self):
-        '''Checks that all known test lists are parseable.'''
-        # This test is located in a full source checkout (e.g.,
-        # src/third_party/autotest/files/client/cros/factory/
-        # factory_unittest.py).  Construct the paths to the reference test list
-        # and any test lists in private overlays.
-        test_lists = [
-            os.path.join(SRCROOT,
-                         'src/third_party/autotest/files/client/site_tests/'
-                         'suite_Factory/test_list.all')
-            ]
+  def test_parse_test_lists(self):
+    '''Checks that all known test lists are parseable.'''
+    # This test is located in a full source checkout (e.g.,
+    # src/third_party/autotest/files/client/cros/factory/
+    # factory_unittest.py). Construct the paths to the reference test list
+    # and any test lists in private overlays.
+    test_lists = [
+      os.path.join(SRCROOT,
+             'src/third_party/autotest/files/client/site_tests/'
+             'suite_Factory/test_list.all')
+      ]
 
-        test_lists.extend(os.path.realpath(x) for x in glob.glob(
-                os.path.join(SRCROOT, 'src/private-overlays/*/'
-                             'chromeos-base/autotest-private-board/'
-                             'files/test_list*')))
+    test_lists.extend(os.path.realpath(x) for x in glob.glob(
+        os.path.join(SRCROOT, 'src/private-overlays/*/'
+               'chromeos-base/autotest-private-board/'
+               'files/test_list*')))
 
-        failures = []
-        for test_list in test_lists:
-            logging.info('Parsing test list %s', test_list)
-            try:
-                factory.read_test_list(test_list)
-            except:
-                failures.append(test_list)
-                traceback.print_exc()
+    failures = []
+    for test_list in test_lists:
+      logging.info('Parsing test list %s', test_list)
+      try:
+        factory.read_test_list(test_list)
+      except:
+        failures.append(test_list)
+        traceback.print_exc()
 
-        if failures:
-            self.fail('Errors in test lists: %r' % failures)
+    if failures:
+      self.fail('Errors in test lists: %r' % failures)
 
-        self.assertEqual([], failures)
+    self.assertEqual([], failures)
 
-    def test_options(self):
-        base_test_list = 'TEST_LIST = []\n'
+  def test_options(self):
+    base_test_list = 'TEST_LIST = []\n'
 
-        # This is a valid option.
-        factory.read_test_list(
-            text=base_test_list +
-            'options.auto_run_on_start = True')
+    # This is a valid option.
+    factory.read_test_list(
+      text=base_test_list +
+      'options.auto_run_on_start = True')
 
-        try:
-            factory.read_test_list(
-                text=base_test_list + 'options.auto_run_on_start = 3')
-            self.fail('Expected exception')
-        except factory.TestListError as e:
-            self.assertTrue(
-                'Option auto_run_on_start has unexpected type' in e[0], e)
+    try:
+      factory.read_test_list(
+        text=base_test_list + 'options.auto_run_on_start = 3')
+      self.fail('Expected exception')
+    except factory.TestListError as e:
+      self.assertTrue(
+        'Option auto_run_on_start has unexpected type' in e[0], e)
 
-        try:
-            factory.read_test_list(
-                text=base_test_list + 'options.fly_me_to_the_moon = 3')
-            self.fail('Expected exception')
-        except factory.TestListError as e:
-            # Sorry, swinging among the stars is currently unsupported.
-            self.assertTrue(
-                'Unknown option fly_me_to_the_moon' in e[0], e)
+    try:
+      factory.read_test_list(
+        text=base_test_list + 'options.fly_me_to_the_moon = 3')
+      self.fail('Expected exception')
+    except factory.TestListError as e:
+      # Sorry, swinging among the stars is currently unsupported.
+      self.assertTrue(
+        'Unknown option fly_me_to_the_moon' in e[0], e)
 
 if __name__ == "__main__":
-    factory.init_logging('factory_unittest')
-    unittest.main()
+  factory.init_logging('factory_unittest')
+  unittest.main()
diff --git a/py/test/gooftools.py b/py/test/gooftools.py
index c9894c7..337c6e0 100644
--- a/py/test/gooftools.py
+++ b/py/test/gooftools.py
@@ -15,7 +15,7 @@
 import sys
 import tempfile
 
-import factory_common
+import factory_common  # pylint: disable=W0611
 from autotest_lib.client.common_lib import error
 from cros.factory.test import factory
 
diff --git a/py/test/leds.py b/py/test/leds.py
index 0e4a913..597236f 100644
--- a/py/test/leds.py
+++ b/py/test/leds.py
@@ -14,7 +14,7 @@
 import sys
 import threading
 
-import factory_common
+import factory_common  # pylint: disable=W0611
 from autotest_lib.client.bin import utils
 
 
diff --git a/py/test/media_util_unittest.py b/py/test/media_util_unittest.py
index 70136e8..23826fa 100755
--- a/py/test/media_util_unittest.py
+++ b/py/test/media_util_unittest.py
@@ -3,7 +3,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import factory_common
+import factory_common  # pylint: disable=W0611
 
 import commands
 import glib
diff --git a/py/test/pytests/execpython.py b/py/test/pytests/execpython.py
index f98df72..d1c25b8 100644
--- a/py/test/pytests/execpython.py
+++ b/py/test/pytests/execpython.py
@@ -1,15 +1,18 @@
+# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 import logging
 import unittest
 
 class ExecPythonTest(unittest.TestCase):
-    '''A simple test that just executes a Python script.
+  '''A simple test that just executes a Python script.
 
-    Args:
-        script: The Python code to execute.
-    '''
-    def runTest(self):
-        script = self.test_info.args['script']
-        logging.info("Executing Python script: '''%s'''", script)
-        exec script in {'test_info': self.test_info}, {}
-        logging.info("Script succeeded")
+  Args:
+    script: The Python code to execute.
+  '''
+  def runTest(self):
+    script = self.test_info.args['script']
+    logging.info("Executing Python script: '''%s'''", script)
+    exec script in {'test_info': self.test_info}, {}
+    logging.info("Script succeeded")
diff --git a/py/test/shopfloor.py b/py/test/shopfloor.py
index db85697..ae9575e 100644
--- a/py/test/shopfloor.py
+++ b/py/test/shopfloor.py
@@ -9,12 +9,12 @@
 factory shop floor system.
 
 The common flow is:
-    - Sets shop floor server URL by shopfloor.set_server_url(url).
-    - Tries shopfllor.check_serial_number(sn) until a valid value is found.
-    - Calls shopfloor.set_enabled(True) to notify other tests.
-    - Gets data by shopfloor.get_*() (ex, get_hwid()).
-    - Uploads reports by shopfloor.upload_report(blob, name).
-    - Finalize by shopfloor.finalize()
+  - Sets shop floor server URL by shopfloor.set_server_url(url).
+  - Tries shopfllor.check_serial_number(sn) until a valid value is found.
+  - Calls shopfloor.set_enabled(True) to notify other tests.
+  - Gets data by shopfloor.get_*() (ex, get_hwid()).
+  - Uploads reports by shopfloor.upload_report(blob, name).
+  - Finalize by shopfloor.finalize()
 
 For the protocol details, check:
  src/platform/factory-utils/factory_setup/shopfloor_server.
@@ -26,7 +26,7 @@
 import xmlrpclib
 from xmlrpclib import Binary, Fault
 
-import factory_common
+import factory_common # pylint: disable=W0611
 from cros.factory.utils import net_utils
 from cros.factory.test import factory
 
@@ -47,7 +47,7 @@
 _DEFAULT_SERVER_PORT = 8082
 
 # Environment variable containing the shopfloor server URL (for
-# testing).  Setting this overrides the shopfloor server URL and
+# testing). Setting this overrides the shopfloor server URL and
 # causes the shopfloor server to be considered enabled.
 SHOPFLOOR_SERVER_ENV_VAR_NAME = 'CROS_SHOPFLOOR_SERVER_URL'
 
@@ -55,140 +55,140 @@
 # Exception Types
 
 class ServerFault(Exception):
-    pass
+  pass
 
 
 def _server_api(call):
-    """Decorator of calls to remote server.
+  """Decorator of calls to remote server.
 
-    Converts xmlrpclib.Fault generated during remote procedural call to better
-    and simplified form (shopfloor.ServerFault).
-    """
-    def wrapped_call(*args, **kargs):
-        try:
-            return call(*args, **kargs)
-        except xmlrpclib.Fault as e:
-            logging.exception('Shopfloor server:')
-            raise ServerFault(e.faultString.partition(':')[2])
-    wrapped_call.__name__ = call.__name__
-    return wrapped_call
+  Converts xmlrpclib.Fault generated during remote procedural call to better
+  and simplified form (shopfloor.ServerFault).
+  """
+  def wrapped_call(*args, **kargs):
+    try:
+      return call(*args, **kargs)
+    except xmlrpclib.Fault as e:
+      logging.exception('Shopfloor server:')
+      raise ServerFault(e.faultString.partition(':')[2])
+  wrapped_call.__name__ = call.__name__
+  return wrapped_call
 
 # ----------------------------------------------------------------------------
 # Utility Functions
 
 def _fetch_current_session():
-    """Gets current shop floor session from factory states shared data.
+  """Gets current shop floor session from factory states shared data.
 
-    If no session is stored yet, create a new default session.
-    """
-    if factory.has_shared_data(KEY_SHOPFLOOR_SESSION):
-        session = factory.get_shared_data(KEY_SHOPFLOOR_SESSION)
-    else:
-        session = {SESSION_SERIAL_NUMBER: None,
-                   SESSION_SERVER_URL: None,
-                   SESSION_ENABLED: False}
-        factory.set_shared_data(KEY_SHOPFLOOR_SESSION, session)
-    return session
+  If no session is stored yet, create a new default session.
+  """
+  if factory.has_shared_data(KEY_SHOPFLOOR_SESSION):
+    session = factory.get_shared_data(KEY_SHOPFLOOR_SESSION)
+  else:
+    session = {SESSION_SERIAL_NUMBER: None,
+          SESSION_SERVER_URL: None,
+          SESSION_ENABLED: False}
+    factory.set_shared_data(KEY_SHOPFLOOR_SESSION, session)
+  return session
 
 
 def _set_session(key, value):
-    """Sets shop floor session value to factory states shared data."""
-    # Currently there's no locking/transaction mechanism in factory shared_data,
-    # so there may be race-condition issue if multiple background tests try to
-    # set shop floor session data at the same time.  However since shop floor
-    # session should be singularily configured in the very beginning, let's fix
-    # this only if that really becomes an issue.
-    session = _fetch_current_session()
-    assert key in session, "Unknown session key: %s" % key
-    session[key] = value
-    factory.set_shared_data(KEY_SHOPFLOOR_SESSION, session)
+  """Sets shop floor session value to factory states shared data."""
+  # Currently there's no locking/transaction mechanism in factory shared_data,
+  # so there may be race-condition issue if multiple background tests try to
+  # set shop floor session data at the same time. However since shop floor
+  # session should be singularily configured in the very beginning, let's fix
+  # this only if that really becomes an issue.
+  session = _fetch_current_session()
+  assert key in session, "Unknown session key: %s" % key
+  session[key] = value
+  factory.set_shared_data(KEY_SHOPFLOOR_SESSION, session)
 
 
 def _get_session(key):
-    """Gets shop floor session value from factory states shared data."""
-    session = _fetch_current_session()
-    assert key in session, "Unknown session key: %s" % key
-    return session[key]
+  """Gets shop floor session value from factory states shared data."""
+  session = _fetch_current_session()
+  assert key in session, "Unknown session key: %s" % key
+  return session[key]
 
 
 def reset():
-    """Resets session data from factory states shared data."""
-    if factory.has_shared_data(KEY_SHOPFLOOR_SESSION):
-        factory.del_shared_data(KEY_SHOPFLOOR_SESSION)
+  """Resets session data from factory states shared data."""
+  if factory.has_shared_data(KEY_SHOPFLOOR_SESSION):
+    factory.del_shared_data(KEY_SHOPFLOOR_SESSION)
 
 
 def is_enabled():
-    """Checks if current factory is configured to use shop floor system."""
-    return (bool(os.environ.get(SHOPFLOOR_SERVER_ENV_VAR_NAME)) or
-            _get_session(SESSION_ENABLED))
+  """Checks if current factory is configured to use shop floor system."""
+  return (bool(os.environ.get(SHOPFLOOR_SERVER_ENV_VAR_NAME)) or
+      _get_session(SESSION_ENABLED))
 
 
 def set_enabled(enabled):
-    """Enable/disable using shop floor in current factory flow."""
-    _set_session(SESSION_ENABLED, enabled)
+  """Enable/disable using shop floor in current factory flow."""
+  _set_session(SESSION_ENABLED, enabled)
 
 
 def set_server_url(url):
-    """Sets default shop floor server URL for further calls."""
-    _set_session(SESSION_SERVER_URL, url)
+  """Sets default shop floor server URL for further calls."""
+  _set_session(SESSION_SERVER_URL, url)
 
 
 def get_server_url():
-    """Gets last configured shop floor server URL."""
-    return (os.environ.get(SHOPFLOOR_SERVER_ENV_VAR_NAME) or
-            _get_session(SESSION_SERVER_URL))
+  """Gets last configured shop floor server URL."""
+  return (os.environ.get(SHOPFLOOR_SERVER_ENV_VAR_NAME) or
+      _get_session(SESSION_SERVER_URL))
 
 
 def detect_default_server_url():
-    """Tries to find a default shop floor server URL.
+  """Tries to find a default shop floor server URL.
 
-       Searches from lsb-* files and deriving from mini-omaha server location.
-    """
-    lsb_values = factory.get_lsb_data()
-    # FACTORY_OMAHA_URL is written by factory_install/factory_install.sh
-    omaha_url = lsb_values.get('FACTORY_OMAHA_URL', None)
-    if omaha_url:
-        omaha = urlparse.urlsplit(omaha_url)
-        netloc = '%s:%s' % (omaha.netloc.split(':')[0], _DEFAULT_SERVER_PORT)
-        return urlparse.urlunsplit((omaha.scheme, netloc, '/', '', ''))
-    return None
+    Searches from lsb-* files and deriving from mini-omaha server location.
+  """
+  lsb_values = factory.get_lsb_data()
+  # FACTORY_OMAHA_URL is written by factory_install/factory_install.sh
+  omaha_url = lsb_values.get('FACTORY_OMAHA_URL', None)
+  if omaha_url:
+    omaha = urlparse.urlsplit(omaha_url)
+    netloc = '%s:%s' % (omaha.netloc.split(':')[0], _DEFAULT_SERVER_PORT)
+    return urlparse.urlunsplit((omaha.scheme, netloc, '/', '', ''))
+  return None
 
 
 def get_instance(url=None, detect=False, timeout=None):
-    """Gets an instance (for client side) to access the shop floor server.
+  """Gets an instance (for client side) to access the shop floor server.
 
-    @param url: URL of the shop floor server. If None, use the value in
-            factory shared data.
-    @param detect: If True, attempt to detect the server URL if none is
-        specified.
-    @param timeout: If not None, the timeout in seconds.
-    @return An object with all public functions from shopfloor.ShopFloorBase.
-    """
-    if not url:
-        url = get_server_url()
-    if not url and detect:
-        url = detect_default_server_url()
-    if not url:
-        raise Exception("Shop floor server URL is NOT configured.")
-    return net_utils.TimeoutXMLRPCServerProxy(
-        url, allow_none=True, verbose=False, timeout=timeout)
+  @param url: URL of the shop floor server. If None, use the value in
+      factory shared data.
+  @param detect: If True, attempt to detect the server URL if none is
+    specified.
+  @param timeout: If not None, the timeout in seconds.
+  @return An object with all public functions from shopfloor.ShopFloorBase.
+  """
+  if not url:
+    url = get_server_url()
+  if not url and detect:
+    url = detect_default_server_url()
+  if not url:
+    raise Exception("Shop floor server URL is NOT configured.")
+  return net_utils.TimeoutXMLRPCServerProxy(
+    url, allow_none=True, verbose=False, timeout=timeout)
 
 
 @_server_api
 def check_server_status(instance=None):
-    """Checks if the given instance is successfully connected.
+  """Checks if the given instance is successfully connected.
 
-    @param instance: Instance object created get_instance, or None to create a
-            new instance.
-    @return True for success, otherwise raise exception.
-    """
-    try:
-        if instance is not None:
-            instance = get_instance()
-        instance.Ping()
-    except:
-        raise
-    return True
+  @param instance: Instance object created get_instance, or None to create a
+      new instance.
+  @return True for success, otherwise raise exception.
+  """
+  try:
+    if instance is not None:
+      instance = get_instance()
+    instance.Ping()
+  except:
+    raise
+  return True
 
 
 # ----------------------------------------------------------------------------
@@ -198,48 +198,48 @@
 
 @_server_api
 def set_serial_number(serial_number):
-    """Sets a serial number as pinned in factory shared data."""
-    _set_session(SESSION_SERIAL_NUMBER, serial_number)
+  """Sets a serial number as pinned in factory shared data."""
+  _set_session(SESSION_SERIAL_NUMBER, serial_number)
 
 
 @_server_api
 def get_serial_number():
-    """Gets current pinned serial number from factory shared data."""
-    return _get_session(SESSION_SERIAL_NUMBER)
+  """Gets current pinned serial number from factory shared data."""
+  return _get_session(SESSION_SERIAL_NUMBER)
 
 
 @_server_api
 def check_serial_number(serial_number):
-    """Checks if given serial number is valid."""
-    # Use GetHWID to check serial number.
-    return get_instance().GetHWID(serial_number)
+  """Checks if given serial number is valid."""
+  # Use GetHWID to check serial number.
+  return get_instance().GetHWID(serial_number)
 
 
 @_server_api
 def get_hwid():
-    """Gets HWID associated with current pinned serial number."""
-    return get_instance().GetHWID(get_serial_number())
+  """Gets HWID associated with current pinned serial number."""
+  return get_instance().GetHWID(get_serial_number())
 
 
 @_server_api
 def get_vpd():
-    """Gets VPD associated with current pinned serial number."""
-    return get_instance().GetVPD(get_serial_number())
+  """Gets VPD associated with current pinned serial number."""
+  return get_instance().GetVPD(get_serial_number())
 
 
 @_server_api
 def upload_report(blob, name=None):
-    """Uploads a report (generated by gooftool) to shop floor server.
+  """Uploads a report (generated by gooftool) to shop floor server.
 
-    @param blob: The report (usually a gzipped bitstream) data to upload.
-    @param name: An optional file name suggestion for server. Usually this
-        should be the default file name created by gooftool; for reports
-        generated by other tools, None allows server to choose arbitrary name.
-    """
-    get_instance().UploadReport(get_serial_number(), Binary(blob), name)
+  @param blob: The report (usually a gzipped bitstream) data to upload.
+  @param name: An optional file name suggestion for server. Usually this
+    should be the default file name created by gooftool; for reports
+    generated by other tools, None allows server to choose arbitrary name.
+  """
+  get_instance().UploadReport(get_serial_number(), Binary(blob), name)
 
 
 @_server_api
 def finalize():
-    """Notifies shop floor server this DUT has finished testing."""
-    get_instance().Finalize(get_serial_number())
+  """Notifies shop floor server this DUT has finished testing."""
+  get_instance().Finalize(get_serial_number())
diff --git a/py/test/state.py b/py/test/state.py
index 420ef4d..253b15e 100644
--- a/py/test/state.py
+++ b/py/test/state.py
@@ -25,7 +25,7 @@
 from hashlib import sha1
 from uuid import uuid4
 
-import factory_common
+import factory_common # pylint: disable=W0611
 
 from jsonrpclib import jsonclass
 from jsonrpclib import jsonrpc
@@ -43,484 +43,484 @@
 
 
 def _synchronized(f):
-    '''
-    Decorates a function to grab a lock.
-    '''
-    def wrapped(self, *args, **kw):
-        with self._lock:  # pylint: disable=W0212
-            return f(self, *args, **kw)
-    return wrapped
+  '''
+  Decorates a function to grab a lock.
+  '''
+  def wrapped(self, *args, **kw):
+    with self._lock: # pylint: disable=W0212
+      return f(self, *args, **kw)
+  return wrapped
 
 
 def clear_state(state_file_path=None):
-    '''Clears test state (removes the state file path).
+  '''Clears test state (removes the state file path).
 
-    Args:
-        state_file_path: Path to state; uses the default path if None.
-    '''
-    state_file_path = state_file_path or DEFAULT_FACTORY_STATE_FILE_PATH
-    logging.warn('Clearing state file path %s' % state_file_path)
-    if os.path.exists(state_file_path):
-        shutil.rmtree(state_file_path)
+  Args:
+    state_file_path: Path to state; uses the default path if None.
+  '''
+  state_file_path = state_file_path or DEFAULT_FACTORY_STATE_FILE_PATH
+  logging.warn('Clearing state file path %s' % state_file_path)
+  if os.path.exists(state_file_path):
+    shutil.rmtree(state_file_path)
 
 
 class TestHistoryItem(object):
-    def __init__(self, path, state, log, trace=None):
-        self.path = path
-        self.state = state
-        self.log = log
-        self.trace = trace
-        self.time = time.time()
+  def __init__(self, path, state, log, trace=None):
+    self.path = path
+    self.state = state
+    self.log = log
+    self.trace = trace
+    self.time = time.time()
 
 
 class PathResolver(object):
-    '''Resolves paths in URLs.'''
-    def __init__(self):
-        self._paths = {}
+  '''Resolves paths in URLs.'''
+  def __init__(self):
+    self._paths = {}
 
-    def AddPath(self, url_path, local_path):
-        '''Adds a prefix mapping:
+  def AddPath(self, url_path, local_path):
+    '''Adds a prefix mapping:
 
-        For example,
+    For example,
 
-            AddPath('/foo', '/usr/local/docs')
+      AddPath('/foo', '/usr/local/docs')
 
-        will cause paths to resolved as follows:
+    will cause paths to resolved as follows:
 
-            /foo            -> /usr/local/docs
-            /foo/index.html -> /usr/local/docs/index.html
+      /foo      -> /usr/local/docs
+      /foo/index.html -> /usr/local/docs/index.html
 
-        Args:
-            url_path: The path in the URL
-        '''
-        self._paths[url_path] = local_path
+    Args:
+      url_path: The path in the URL
+    '''
+    self._paths[url_path] = local_path
 
-    def Resolve(self, url_path):
-        '''Resolves a path mapping.
+  def Resolve(self, url_path):
+    '''Resolves a path mapping.
 
-        Returns None if no paths match.'
+    Returns None if no paths match.'
 
-        Args:
-            url_path: A path in a URL (starting with /).
-        '''
-        if not url_path.startswith('/'):
-            return None
+    Args:
+      url_path: A path in a URL (starting with /).
+    '''
+    if not url_path.startswith('/'):
+      return None
 
-        prefix = url_path
-        while prefix != '':
-            local_prefix = self._paths.get(prefix)
-            if local_prefix:
-                return local_prefix + url_path[len(prefix):]
-            prefix, _, _ = prefix.rpartition('/')
+    prefix = url_path
+    while prefix != '':
+      local_prefix = self._paths.get(prefix)
+      if local_prefix:
+        return local_prefix + url_path[len(prefix):]
+      prefix, _, _ = prefix.rpartition('/')
 
-        root_prefix = self._paths.get('/')
-        if root_prefix:
-            return root_prefix + url_path
+    root_prefix = self._paths.get('/')
+    if root_prefix:
+      return root_prefix + url_path
 
 
 @unicode_to_string.UnicodeToStringClass
 class FactoryState(object):
+  '''
+  The core implementation for factory state control.
+  The major provided features are:
+
+  SHARED DATA
+    You can get/set simple data into the states and share between all tests.
+    See get_shared_data(name) and set_shared_data(name, value) for more
+    information.
+
+  TEST STATUS
+    To track the execution status of factory auto tests, you can use
+    get_test_state, get_test_states methods, and update_test_state
+    methods.
+
+  All arguments may be provided either as strings, or as Unicode strings in
+  which case they are converted to strings using UTF-8. All returned values
+  are strings (not Unicode).
+
+  This object is thread-safe.
+
+  See help(FactoryState.[methodname]) for more information.
+
+  Properties:
+    _generated_files: Map from UUID to paths on disk. These are
+      not persisted on disk (though they could be if necessary).
+    _generated_data: Map from UUID to (mime_type, data) pairs for
+      transient objects to serve.
+    _generated_data_expiration: Priority queue of expiration times
+      for objects in _generated_data.
+  '''
+
+  def __init__(self, state_file_path=None):
     '''
-    The core implementation for factory state control.
-    The major provided features are:
+    Initializes the state server.
 
-    SHARED DATA
-        You can get/set simple data into the states and share between all tests.
-        See get_shared_data(name) and set_shared_data(name, value) for more
-        information.
-
-    TEST STATUS
-        To track the execution status of factory auto tests, you can use
-        get_test_state, get_test_states methods, and update_test_state
-        methods.
-
-    All arguments may be provided either as strings, or as Unicode strings in
-    which case they are converted to strings using UTF-8.  All returned values
-    are strings (not Unicode).
-
-    This object is thread-safe.
-
-    See help(FactoryState.[methodname]) for more information.
-
-    Properties:
-        _generated_files: Map from UUID to paths on disk.  These are
-            not persisted on disk (though they could be if necessary).
-        _generated_data: Map from UUID to (mime_type, data) pairs for
-            transient objects to serve.
-        _generated_data_expiration: Priority queue of expiration times
-            for objects in _generated_data.
+    Parameters:
+      state_file_path:  External file to store the state information.
     '''
+    state_file_path = state_file_path or DEFAULT_FACTORY_STATE_FILE_PATH
+    if not os.path.exists(state_file_path):
+      os.makedirs(state_file_path)
+    self._tests_shelf = shelve.open(state_file_path + '/tests')
+    self._data_shelf = shelve.open(state_file_path + '/data')
+    self._test_history_shelf = shelve.open(state_file_path +
+                        '/test_history')
+    self._lock = threading.RLock()
+    self.test_list_struct = None
 
-    def __init__(self, state_file_path=None):
-        '''
-        Initializes the state server.
+    self._generated_files = {}
+    self._generated_data = {}
+    self._generated_data_expiration = Queue.PriorityQueue()
+    self._resolver = PathResolver()
 
-        Parameters:
-            state_file_path:    External file to store the state information.
-        '''
-        state_file_path = state_file_path or DEFAULT_FACTORY_STATE_FILE_PATH
-        if not os.path.exists(state_file_path):
-            os.makedirs(state_file_path)
-        self._tests_shelf = shelve.open(state_file_path + '/tests')
-        self._data_shelf = shelve.open(state_file_path + '/data')
-        self._test_history_shelf = shelve.open(state_file_path +
-                                               '/test_history')
-        self._lock = threading.RLock()
-        self.test_list_struct = None
+    if TestState not in jsonclass.supported_types:
+      jsonclass.supported_types.append(TestState)
 
-        self._generated_files = {}
-        self._generated_data = {}
-        self._generated_data_expiration = Queue.PriorityQueue()
-        self._resolver = PathResolver()
+  @_synchronized
+  def close(self):
+    '''
+    Shuts down the state instance.
+    '''
+    for shelf in [self._tests_shelf,
+           self._data_shelf,
+           self._test_history_shelf]:
+      try:
+        shelf.close()
+      except:
+        logging.exception('Unable to close shelf')
 
-        if TestState not in jsonclass.supported_types:
-            jsonclass.supported_types.append(TestState)
+  @_synchronized
+  def update_test_state(self, path, **kw):
+    '''
+    Updates the state of a test.
 
-    @_synchronized
-    def close(self):
-        '''
-        Shuts down the state instance.
-        '''
-        for shelf in [self._tests_shelf,
-                      self._data_shelf,
-                      self._test_history_shelf]:
-            try:
-                shelf.close()
-            except:
-                logging.exception('Unable to close shelf')
+    See TestState.update for the allowable keyword arguments.
 
-    @_synchronized
-    def update_test_state(self, path, **kw):
-        '''
-        Updates the state of a test.
+    @param path: The path to the test (see FactoryTest for a description
+      of test paths).
+    @param kw: See TestState.update for allowable arguments (e.g.,
+      status and increment_count).
 
-        See TestState.update for the allowable keyword arguments.
+    @return: A tuple containing the new state, and a boolean indicating
+      whether the state was just changed.
+    '''
+    state = self._tests_shelf.get(path)
+    old_state_repr = repr(state)
+    changed = False
 
-        @param path: The path to the test (see FactoryTest for a description
-            of test paths).
-        @param kw: See TestState.update for allowable arguments (e.g.,
-            status and increment_count).
+    if not state:
+      changed = True
+      state = TestState()
 
-        @return: A tuple containing the new state, and a boolean indicating
-            whether the state was just changed.
-        '''
-        state = self._tests_shelf.get(path)
-        old_state_repr = repr(state)
-        changed = False
+    changed = changed | state.update(**kw) # Don't short-circuit
 
-        if not state:
-            changed = True
-            state = TestState()
+    if changed:
+      logging.debug('Updating test state for %s: %s -> %s',
+             path, old_state_repr, state)
+      self._tests_shelf[path] = state
+      self._tests_shelf.sync()
 
-        changed = changed | state.update(**kw)  # Don't short-circuit
+    return state, changed
 
-        if changed:
-            logging.debug('Updating test state for %s: %s -> %s',
-                          path, old_state_repr, state)
-            self._tests_shelf[path] = state
-            self._tests_shelf.sync()
+  @_synchronized
+  def get_test_state(self, path):
+    '''
+    Returns the state of a test.
+    '''
+    return self._tests_shelf[path]
 
-        return state, changed
+  @_synchronized
+  def get_test_paths(self):
+    '''
+    Returns a list of all tests' paths.
+    '''
+    return self._tests_shelf.keys()
 
-    @_synchronized
-    def get_test_state(self, path):
-        '''
-        Returns the state of a test.
-        '''
-        return self._tests_shelf[path]
+  @_synchronized
+  def get_test_states(self):
+    '''
+    Returns a map of each test's path to its state.
+    '''
+    return dict(self._tests_shelf)
 
-    @_synchronized
-    def get_test_paths(self):
-        '''
-        Returns a list of all tests' paths.
-        '''
-        return self._tests_shelf.keys()
+  def get_test_list(self):
+    '''
+    Returns the test list.
+    '''
+    return self.test_list.to_struct()
 
-    @_synchronized
-    def get_test_states(self):
-        '''
-        Returns a map of each test's path to its state.
-        '''
-        return dict(self._tests_shelf)
+  @_synchronized
+  def set_shared_data(self, *key_value_pairs):
+    '''
+    Sets shared data items.
 
-    def get_test_list(self):
-        '''
-        Returns the test list.
-        '''
-        return self.test_list.to_struct()
+    Args:
+      key_value_pairs: A series of alternating keys and values
+        (k1, v1, k2, v2...). In the simple case this can just
+        be a single key and value.
+    '''
+    assert len(key_value_pairs) % 2 == 0, repr(key_value_pairs)
+    for i in range(0, len(key_value_pairs), 2):
+      self._data_shelf[key_value_pairs[i]] = key_value_pairs[i + 1]
+    self._data_shelf.sync()
 
-    @_synchronized
-    def set_shared_data(self, *key_value_pairs):
-        '''
-        Sets shared data items.
+  @_synchronized
+  def get_shared_data(self, key, optional=False):
+    '''
+    Retrieves a shared data item.
 
-        Args:
-            key_value_pairs: A series of alternating keys and values
-                (k1, v1, k2, v2...).  In the simple case this can just
-                be a single key and value.
-        '''
-        assert len(key_value_pairs) % 2 == 0, repr(key_value_pairs)
-        for i in range(0, len(key_value_pairs), 2):
-            self._data_shelf[key_value_pairs[i]] = key_value_pairs[i + 1]
-        self._data_shelf.sync()
+    Args:
+      key: The key whose value to retrieve.
+      optional: True to return None if not found; False to raise
+        a KeyError.
+    '''
+    if optional:
+      return self._data_shelf.get(key)
+    else:
+      return self._data_shelf[key]
 
-    @_synchronized
-    def get_shared_data(self, key, optional=False):
-        '''
-        Retrieves a shared data item.
+  @_synchronized
+  def has_shared_data(self, key):
+    '''
+    Returns if a shared data item exists.
+    '''
+    return key in self._data_shelf
 
-        Args:
-            key: The key whose value to retrieve.
-            optional: True to return None if not found; False to raise
-                a KeyError.
-        '''
-        if optional:
-            return self._data_shelf.get(key)
-        else:
-            return self._data_shelf[key]
+  @_synchronized
+  def del_shared_data(self, key, optional=False):
+    '''
+    Deletes a shared data item.
 
-    @_synchronized
-    def has_shared_data(self, key):
-        '''
-        Returns if a shared data item exists.
-        '''
-        return key in self._data_shelf
+    Args:
+      key: The key whose value to retrieve.
+      optional: False to raise a KeyError if not found.
+    '''
+    try:
+      del self._data_shelf[key]
+    except KeyError:
+      if not optional:
+        raise
 
-    @_synchronized
-    def del_shared_data(self, key, optional=False):
-        '''
-        Deletes a shared data item.
+  @_synchronized
+  def add_test_history(self, history_item):
+    path = history_item.path
+    assert path
 
-        Args:
-            key: The key whose value to retrieve.
-            optional: False to raise a KeyError if not found.
-        '''
+    length_key = path + '[length]'
+    num_entries = self._test_history_shelf.get(length_key, 0)
+    self._test_history_shelf[path + '[%d]' % num_entries] = history_item
+    self._test_history_shelf[length_key] = num_entries + 1
+
+  @_synchronized
+  def get_test_history(self, paths):
+    if type(paths) != list:
+      paths = [paths]
+    ret = []
+
+    for path in paths:
+      i = 0
+      while True:
+        value = self._test_history_shelf.get(path + '[%d]' % i)
+
+        i += 1
+        if not value:
+          break
+        ret.append(value)
+
+    ret.sort(key=lambda item: item.time)
+
+    return ret
+
+  @_synchronized
+  def url_for_file(self, path):
+    '''Returns a URL that can be used to serve a local file.
+
+    Args:
+     path: path to the local file
+
+    Returns:
+     url: A (possibly relative) URL that refers to the file
+    '''
+    uuid = str(uuid4())
+    uri_path = '/generated-files/%s/%s' % (uuid, os.path.basename(path))
+    self._generated_files[uuid] = path
+    return uri_path
+
+  @_synchronized
+  def url_for_data(self, mime_type, data, expiration_secs=None):
+    '''Returns a URL that can be used to serve a static collection
+    of bytes.
+
+    Args:
+     mime_type: MIME type for the data
+     data: Data to serve
+     expiration_secs: If not None, the number of seconds in which
+      the data will expire.
+    '''
+    uuid = str(uuid4())
+    self._generated_data[uuid] = mime_type, data
+    if expiration_secs:
+      now = time.time()
+      self._generated_data_expiration.put(
+        (now + expiration_secs, uuid))
+
+      # Reap old items.
+      while True:
         try:
-            del self._data_shelf[key]
-        except KeyError:
-            if not optional:
-                raise
+          item = self._generated_data_expiration.get_nowait()
+        except Queue.Empty:
+          break
 
-    @_synchronized
-    def add_test_history(self, history_item):
-        path = history_item.path
-        assert path
+        if item[0] < now:
+          del self._generated_data[item[1]]
+        else:
+          # Not expired yet; put it back and we're done
+          self._generated_data_expiration.put(item)
+          break
+    uri_path = '/generated-data/%s' % uuid
+    return uri_path
 
-        length_key = path + '[length]'
-        num_entries = self._test_history_shelf.get(length_key, 0)
-        self._test_history_shelf[path + '[%d]' % num_entries] = history_item
-        self._test_history_shelf[length_key] = num_entries + 1
+  @_synchronized
+  def register_path(self, url_path, local_path):
+    self._resolver.AddPath(url_path, local_path)
 
-    @_synchronized
-    def get_test_history(self, paths):
-        if type(paths) != list:
-            paths = [paths]
-        ret = []
+  def get_system_status(self):
+    '''Returns system status information.
 
-        for path in paths:
-            i = 0
-            while True:
-                value = self._test_history_shelf.get(path + '[%d]' % i)
-
-                i += 1
-                if not value:
-                    break
-                ret.append(value)
-
-        ret.sort(key=lambda item: item.time)
-
-        return ret
-
-    @_synchronized
-    def url_for_file(self, path):
-        '''Returns a URL that can be used to serve a local file.
-
-        Args:
-          path: path to the local file
-
-        Returns:
-          url: A (possibly relative) URL that refers to the file
-        '''
-        uuid = str(uuid4())
-        uri_path = '/generated-files/%s/%s' % (uuid, os.path.basename(path))
-        self._generated_files[uuid] = path
-        return uri_path
-
-    @_synchronized
-    def url_for_data(self, mime_type, data, expiration_secs=None):
-        '''Returns a URL that can be used to serve a static collection
-        of bytes.
-
-        Args:
-          mime_type: MIME type for the data
-          data: Data to serve
-          expiration_secs: If not None, the number of seconds in which
-            the data will expire.
-        '''
-        uuid = str(uuid4())
-        self._generated_data[uuid] = mime_type, data
-        if expiration_secs:
-            now = time.time()
-            self._generated_data_expiration.put(
-                (now + expiration_secs, uuid))
-
-            # Reap old items.
-            while True:
-                try:
-                    item = self._generated_data_expiration.get_nowait()
-                except Queue.Empty:
-                    break
-
-                if item[0] < now:
-                    del self._generated_data[item[1]]
-                else:
-                    # Not expired yet; put it back and we're done
-                    self._generated_data_expiration.put(item)
-                    break
-        uri_path = '/generated-data/%s' % uuid
-        return uri_path
-
-    @_synchronized
-    def register_path(self, url_path, local_path):
-        self._resolver.AddPath(url_path, local_path)
-
-    def get_system_status(self):
-        '''Returns system status information.
-
-        This may include system load, battery status, etc.  See
-        system.SystemStatus().
-        '''
-        return system.SystemStatus().__dict__
+    This may include system load, battery status, etc. See
+    system.SystemStatus().
+    '''
+    return system.SystemStatus().__dict__
 
 
 def get_instance(address=DEFAULT_FACTORY_STATE_ADDRESS,
-                 port=DEFAULT_FACTORY_STATE_PORT):
-    '''
-    Gets an instance (for client side) to access the state server.
+         port=DEFAULT_FACTORY_STATE_PORT):
+  '''
+  Gets an instance (for client side) to access the state server.
 
-    @param address: Address of the server to be connected.
-    @param port: Port of the server to be connected.
-    @return An object with all public functions from FactoryState.
-        See help(FactoryState) for more information.
-    '''
-    return jsonrpc.ServerProxy('http://%s:%d' % (address, port),
-                               verbose=False)
+  @param address: Address of the server to be connected.
+  @param port: Port of the server to be connected.
+  @return An object with all public functions from FactoryState.
+    See help(FactoryState) for more information.
+  '''
+  return jsonrpc.ServerProxy('http://%s:%d' % (address, port),
+                verbose=False)
 
 
 class MyJSONRPCRequestHandler(SimpleJSONRPCServer.SimpleJSONRPCRequestHandler):
-    def do_GET(self):
-        logging.debug('HTTP request for path %s', self.path)
+  def do_GET(self):
+    logging.debug('HTTP request for path %s', self.path)
 
-        handler = self.server.handlers.get(self.path)
-        if handler:
-            return handler(self)
+    handler = self.server.handlers.get(self.path)
+    if handler:
+      return handler(self)
 
-        match = re.match('^/generated-data/([-0-9a-f]+)$', self.path)
-        if match:
-            generated_data = self.server._generated_data.get(match.group(1))
-            if not generated_data:
-                logging.warn('Unknown or expired generated data %s',
-                             match.group(1))
-                self.send_response(404)
-                return
+    match = re.match('^/generated-data/([-0-9a-f]+)$', self.path)
+    if match:
+      generated_data = self.server._generated_data.get(match.group(1))
+      if not generated_data:
+        logging.warn('Unknown or expired generated data %s',
+               match.group(1))
+        self.send_response(404)
+        return
 
-            mime_type, data = generated_data
+      mime_type, data = generated_data
 
-            self.send_response(200)
-            self.send_header('Content-Type', mime_type)
-            self.send_header('Content-Length', len(data))
-            self.end_headers()
-            self.wfile.write(data)
+      self.send_response(200)
+      self.send_header('Content-Type', mime_type)
+      self.send_header('Content-Length', len(data))
+      self.end_headers()
+      self.wfile.write(data)
 
-        if self.path.endswith('/'):
-            self.path += 'index.html'
+    if self.path.endswith('/'):
+      self.path += 'index.html'
 
-        if ".." in self.path.split("/"):
-            logging.warn("Invalid path")
-            self.send_response(404)
-            return
+    if ".." in self.path.split("/"):
+      logging.warn("Invalid path")
+      self.send_response(404)
+      return
 
-        mime_type = mimetypes.guess_type(self.path)
-        if not mime_type:
-            logging.warn("Unable to guess MIME type")
-            self.send_response(404)
-            return
+    mime_type = mimetypes.guess_type(self.path)
+    if not mime_type:
+      logging.warn("Unable to guess MIME type")
+      self.send_response(404)
+      return
 
-        local_path = None
-        match = re.match('^/generated-files/([-0-9a-f]+)/', self.path)
-        if match:
-            local_path = self.server._generated_files.get(match.group(1))
-            if not local_path:
-                logging.warn('Unknown generated file %s in path %s',
-                             match.group(1), self.path)
-                self.send_response(404)
-                return
+    local_path = None
+    match = re.match('^/generated-files/([-0-9a-f]+)/', self.path)
+    if match:
+      local_path = self.server._generated_files.get(match.group(1))
+      if not local_path:
+        logging.warn('Unknown generated file %s in path %s',
+               match.group(1), self.path)
+        self.send_response(404)
+        return
 
-        local_path = self.server._resolver.Resolve(self.path)
-        if not local_path or not os.path.exists(local_path):
-            logging.warn("File not found: %s", (local_path or self.path))
-            self.send_response(404)
-            return
+    local_path = self.server._resolver.Resolve(self.path)
+    if not local_path or not os.path.exists(local_path):
+      logging.warn("File not found: %s", (local_path or self.path))
+      self.send_response(404)
+      return
 
-        self.send_response(200)
-        self.send_header("Content-Type", mime_type[0])
-        self.send_header("Content-Length", os.path.getsize(local_path))
-        self.end_headers()
-        with open(local_path) as f:
-            shutil.copyfileobj(f, self.wfile)
+    self.send_response(200)
+    self.send_header("Content-Type", mime_type[0])
+    self.send_header("Content-Length", os.path.getsize(local_path))
+    self.end_headers()
+    with open(local_path) as f:
+      shutil.copyfileobj(f, self.wfile)
 
 
 class ThreadedJSONRPCServer(SocketServer.ThreadingMixIn,
-                            SimpleJSONRPCServer.SimpleJSONRPCServer):
-    '''The JSON/RPC server.
+              SimpleJSONRPCServer.SimpleJSONRPCServer):
+  '''The JSON/RPC server.
 
-    Properties:
-        handlers: A map from URLs to callbacks handling them.  (The callback
-            takes a single argument: the request to handle.)
-    '''
-    def __init__(self, *args, **kwargs):
-        SimpleJSONRPCServer.SimpleJSONRPCServer.__init__(self, *args, **kwargs)
-        self.handlers = {}
+  Properties:
+    handlers: A map from URLs to callbacks handling them. (The callback
+      takes a single argument: the request to handle.)
+  '''
+  def __init__(self, *args, **kwargs):
+    SimpleJSONRPCServer.SimpleJSONRPCServer.__init__(self, *args, **kwargs)
+    self.handlers = {}
 
-    def add_handler(self, url, callback):
-        self.handlers[url] = callback
+  def add_handler(self, url, callback):
+    self.handlers[url] = callback
 
 
 def create_server(state_file_path=None, bind_address=None, port=None):
-    '''
-    Creates a FactoryState object and an JSON/RPC server to serve it.
+  '''
+  Creates a FactoryState object and an JSON/RPC server to serve it.
 
-    @param state_file_path: The path containing the saved state.
-    @param bind_address: Address to bind to, defaulting to
-        DEFAULT_FACTORY_STATE_BIND_ADDRESS.
-    @param port: Port to bind to, defaulting to DEFAULT_FACTORY_STATE_PORT.
-    @return A tuple of the FactoryState instance and the SimpleJSONRPCServer
-        instance.
-    '''
-    # We have some icons in SVG format, but this isn't recognized in
-    # the standard Python mimetypes set.
-    mimetypes.add_type('image/svg+xml', '.svg')
+  @param state_file_path: The path containing the saved state.
+  @param bind_address: Address to bind to, defaulting to
+    DEFAULT_FACTORY_STATE_BIND_ADDRESS.
+  @param port: Port to bind to, defaulting to DEFAULT_FACTORY_STATE_PORT.
+  @return A tuple of the FactoryState instance and the SimpleJSONRPCServer
+    instance.
+  '''
+  # We have some icons in SVG format, but this isn't recognized in
+  # the standard Python mimetypes set.
+  mimetypes.add_type('image/svg+xml', '.svg')
 
-    if not bind_address:
-        bind_address = DEFAULT_FACTORY_STATE_BIND_ADDRESS
-    if not port:
-        port = DEFAULT_FACTORY_STATE_PORT
-    instance = FactoryState(state_file_path)
-    instance._resolver.AddPath(
-        '/',
-        os.path.join(factory.FACTORY_PACKAGE_PATH, 'goofy/static'))
+  if not bind_address:
+    bind_address = DEFAULT_FACTORY_STATE_BIND_ADDRESS
+  if not port:
+    port = DEFAULT_FACTORY_STATE_PORT
+  instance = FactoryState(state_file_path)
+  instance._resolver.AddPath(
+    '/',
+    os.path.join(factory.FACTORY_PACKAGE_PATH, 'goofy/static'))
 
-    server = ThreadedJSONRPCServer(
-        (bind_address, port),
-        requestHandler=MyJSONRPCRequestHandler,
-        logRequests=False)
+  server = ThreadedJSONRPCServer(
+    (bind_address, port),
+    requestHandler=MyJSONRPCRequestHandler,
+    logRequests=False)
 
-    # Give the server the information it needs to resolve URLs.
-    server._generated_files = instance._generated_files
-    server._generated_data = instance._generated_data
-    server._resolver = instance._resolver
+  # Give the server the information it needs to resolve URLs.
+  server._generated_files = instance._generated_files
+  server._generated_data = instance._generated_data
+  server._resolver = instance._resolver
 
-    server.register_introspection_functions()
-    server.register_instance(instance)
-    server.web_socket_handler = None
-    return instance, server
+  server.register_introspection_functions()
+  server.register_instance(instance)
+  server.web_socket_handler = None
+  return instance, server
diff --git a/py/test/state_unittest.py b/py/test/state_unittest.py
index 7344f8b..b99fc42 100755
--- a/py/test/state_unittest.py
+++ b/py/test/state_unittest.py
@@ -6,35 +6,35 @@
 
 import unittest
 
-import factory_common
+import factory_common # pylint: disable=W0611
 from cros.factory.test.state import PathResolver
 
 
 class PathResolverTest(unittest.TestCase):
-    def testWithRoot(self):
-        resolver = PathResolver()
-        resolver.AddPath('/', '/root')
-        resolver.AddPath('/a/b', '/c/d')
-        resolver.AddPath('/a', '/e')
+  def testWithRoot(self):
+    resolver = PathResolver()
+    resolver.AddPath('/', '/root')
+    resolver.AddPath('/a/b', '/c/d')
+    resolver.AddPath('/a', '/e')
 
-        for url_path, expected_local_path in (
-            ('/', '/root'),
-            ('/a/b', '/c/d'),
-            ('/a', '/e'),
-            ('/a/b/X', '/c/d/X'),
-            ('/a/X', '/e/X'),
-            ('/X', '/root/X'),
-            ('/X/', '/root/X/'),
-            ('/X/Y', '/root/X/Y'),
-            ('Blah', None)):
-            self.assertEqual(expected_local_path,
-                             resolver.Resolve(url_path))
+    for url_path, expected_local_path in (
+      ('/', '/root'),
+      ('/a/b', '/c/d'),
+      ('/a', '/e'),
+      ('/a/b/X', '/c/d/X'),
+      ('/a/X', '/e/X'),
+      ('/X', '/root/X'),
+      ('/X/', '/root/X/'),
+      ('/X/Y', '/root/X/Y'),
+      ('Blah', None)):
+      self.assertEqual(expected_local_path,
+               resolver.Resolve(url_path))
 
-    def testNoRoot(self):
-        resolver = PathResolver()
-        resolver.AddPath('/a/b', '/c/d')
-        self.assertEqual(None, resolver.Resolve('/b'))
-        self.assertEqual('/c/d/X', resolver.Resolve('/a/b/X'))
+  def testNoRoot(self):
+    resolver = PathResolver()
+    resolver.AddPath('/a/b', '/c/d')
+    self.assertEqual(None, resolver.Resolve('/b'))
+    self.assertEqual('/c/d/X', resolver.Resolve('/a/b/X'))
 
 if __name__ == "__main__":
-    unittest.main()
+  unittest.main()
diff --git a/py/test/task.py b/py/test/task.py
index 4953458..074e98f 100755
--- a/py/test/task.py
+++ b/py/test/task.py
@@ -17,7 +17,7 @@
 import gobject
 import gtk
 
-import factory_common
+import factory_common  # pylint: disable=W0611
 from cros.factory.test import factory
 from cros.factory.test import ui
 
diff --git a/py/test/test_ui.py b/py/test/test_ui.py
index 3e69ea8..d6b4b4e 100644
--- a/py/test/test_ui.py
+++ b/py/test/test_ui.py
@@ -16,209 +16,209 @@
 
 
 class FactoryTestFailure(Exception):
-    pass
+  pass
 
 
 class UI(object):
-    '''Web UI for a Goofy test.
+  '''Web UI for a Goofy test.
 
-    You can set your test up in the following ways:
+  You can set your test up in the following ways:
 
-    1. For simple tests with just Python+HTML+JS:
+  1. For simple tests with just Python+HTML+JS:
 
-         mytest.py
-         mytest.js    (automatically loaded)
-         mytest.html  (automatically loaded)
+     mytest.py
+     mytest.js  (automatically loaded)
+     mytest.html (automatically loaded)
 
-       This works for autotests too:
+    This works for autotests too:
 
-         factory_MyTest.py
-         factory_MyTest.js    (automatically loaded)
-         factory_MyTest.html  (automatically loaded)
+     factory_MyTest.py
+     factory_MyTest.js  (automatically loaded)
+     factory_MyTest.html (automatically loaded)
 
-    2. If you have more files to include, like images
-       or other JavaScript libraries:
+  2. If you have more files to include, like images
+    or other JavaScript libraries:
 
-         mytest.py
-         mytest_static/
-           mytest.js           (automatically loaded)
-           mytest.html         (automatically loaded)
-           some_js_library.js  (NOT automatically loaded;
-                                use <script src="some_js_lib.js">)
-           some_image.gif      (use <img src="some_image.gif">)
+     mytest.py
+     mytest_static/
+      mytest.js      (automatically loaded)
+      mytest.html     (automatically loaded)
+      some_js_library.js (NOT automatically loaded;
+                use <script src="some_js_lib.js">)
+      some_image.gif   (use <img src="some_image.gif">)
 
-    3. Same as #2, but with a directory just called "static" instead of
-       "mytest_static".  This is nicer if your test is already in a
-       directory that contains the test name (as for autotests).  So
-       for a test called factory_MyTest.py, you might have:
+  3. Same as #2, but with a directory just called "static" instead of
+    "mytest_static". This is nicer if your test is already in a
+    directory that contains the test name (as for autotests). So
+    for a test called factory_MyTest.py, you might have:
 
-         factory_MyTest/
-           factory_MyTest.py
-           static/
-             factory_MyTest.html  (automatically loaded)
-             factory_MyTest.js    (automatically loaded)
-             some_js_library.js
-             some_image.gif
+     factory_MyTest/
+      factory_MyTest.py
+      static/
+       factory_MyTest.html (automatically loaded)
+       factory_MyTest.js  (automatically loaded)
+       some_js_library.js
+       some_image.gif
 
-    Note that if you rename .html or .js files during development, you
-    may need to restart the server for your changes to take effect.
-    '''
-    def __init__(self):
-        self.lock = threading.RLock()
-        self.event_client = EventClient(callback=self._handle_event)
-        self.test = os.environ['CROS_FACTORY_TEST_PATH']
-        self.invocation = os.environ['CROS_FACTORY_TEST_INVOCATION']
-        self.event_handlers = {}
+  Note that if you rename .html or .js files during development, you
+  may need to restart the server for your changes to take effect.
+  '''
+  def __init__(self):
+    self.lock = threading.RLock()
+    self.event_client = EventClient(callback=self._handle_event)
+    self.test = os.environ['CROS_FACTORY_TEST_PATH']
+    self.invocation = os.environ['CROS_FACTORY_TEST_INVOCATION']
+    self.event_handlers = {}
 
-        # Set base URL so that hrefs will resolve properly,
-        # and pull in Goofy CSS.
-        self.append_html('\n'.join([
-                    '<base href="/tests/%s/">' % self.test,
-                    ('<link rel="stylesheet" type="text/css" '
-                     'href="/goofy.css">')]))
-        self._setup_static_files(
-            os.path.realpath(traceback.extract_stack()[-2][0]))
+    # Set base URL so that hrefs will resolve properly,
+    # and pull in Goofy CSS.
+    self.append_html('\n'.join([
+          '<base href="/tests/%s/">' % self.test,
+          ('<link rel="stylesheet" type="text/css" '
+           'href="/goofy.css">')]))
+    self._setup_static_files(
+      os.path.realpath(traceback.extract_stack()[-2][0]))
 
-    def _setup_static_files(self, py_script):
-        # Get path to caller and register static files/directories.
-        base = os.path.splitext(py_script)[0]
+  def _setup_static_files(self, py_script):
+    # Get path to caller and register static files/directories.
+    base = os.path.splitext(py_script)[0]
 
-        # Directories we'll autoload .html and .js files from.
-        autoload_bases = [base]
+    # Directories we'll autoload .html and .js files from.
+    autoload_bases = [base]
 
-        # Find and register the static directory, if any.
-        static_dirs = filter(os.path.exists,
-                             [base + '_static',
-                              os.path.join(os.path.dirname(py_script), 'static')
-                             ])
-        if len(static_dirs) > 1:
-            raise FactoryTestFailure('Cannot have both of %s - delete one!' %
-                                     static_dirs)
-        if static_dirs:
-            factory.get_state_instance().register_path(
-                '/tests/%s' % self.test, static_dirs[0])
-            autoload_bases.append(
-                os.path.join(static_dirs[0], os.path.basename(base)))
+    # Find and register the static directory, if any.
+    static_dirs = filter(os.path.exists,
+               [base + '_static',
+               os.path.join(os.path.dirname(py_script), 'static')
+               ])
+    if len(static_dirs) > 1:
+      raise FactoryTestFailure('Cannot have both of %s - delete one!' %
+                   static_dirs)
+    if static_dirs:
+      factory.get_state_instance().register_path(
+        '/tests/%s' % self.test, static_dirs[0])
+      autoload_bases.append(
+        os.path.join(static_dirs[0], os.path.basename(base)))
 
-        # Autoload .html and .js files.
-        for extension in ('js', 'html'):
-            autoload = filter(os.path.exists,
-                              [x + '.' + extension
-                               for x in autoload_bases])
-            if len(autoload) > 1:
-                raise FactoryTestFailure(
-                    'Cannot have both of %s - delete one!' %
-                    autoload)
-            if autoload:
-                factory.get_state_instance().register_path(
-                    '/tests/%s/%s' % (self.test, os.path.basename(autoload[0])),
-                    autoload[0])
-                if extension == 'html':
-                    self.append_html(open(autoload[0]).read())
-                else:
-                    self.append_html('<script src="%s"></script>' %
-                                     os.path.basename(autoload[0]))
-
-    def set_html(self, html, append=False):
-        '''Sets the UI in the test pane.'''
-        self.event_client.post_event(Event(Event.Type.SET_HTML,
-                                           test=self.test,
-                                           invocation=self.invocation,
-                                           html=html,
-                                           append=append))
-
-    def append_html(self, html):
-        '''Append to the UI in the test pane.'''
-        self.set_html(html, True)
-
-    def run_js(self, js, **kwargs):
-        '''Runs JavaScript code in the UI.
-
-        Args:
-            js: The JavaScript code to execute.
-            kwargs: Arguments to pass to the code; they will be
-                available in an "args" dict within the evaluation
-                context.
-
-        Example:
-            ui.run_js('alert(args.msg)', msg='The British are coming')
-        '''
-        self.event_client.post_event(Event(Event.Type.RUN_JS,
-                                           test=self.test,
-                                           invocation=self.invocation,
-                                           js=js, args=kwargs))
-
-    def call_js_function(self, name, *args):
-        '''Calls a JavaScript function in the test pane.
-
-        This will be run within window scope (i.e., 'this' will be the
-        test pane window).
-
-        Args:
-            name: The name of the function to execute.
-            args: Arguments to the function.
-        '''
-        self.event_client.post_event(Event(Event.Type.CALL_JS_FUNCTION,
-                                           test=self.test,
-                                           invocation=self.invocation,
-                                           name=name, args=args))
-
-    def add_event_handler(self, subtype, handler):
-        '''Adds an event handler.
-
-        Args:
-            subtype: The test-specific type of event to be handled.
-            handler: The handler to invoke with a single argument (the event
-                object).
-        '''
-        self.event_handlers.setdefault(subtype, []).append(handler)
-
-    def url_for_file(self, path):
-        '''Returns a URL that can be used to serve a local file.
-
-        Args:
-          path: path to the local file
-
-        Returns:
-          url: A (possibly relative) URL that refers to the file
-        '''
-        return factory.get_state_instance().url_for_file(path)
-
-    def url_for_data(self, mime_type, data, expiration=None):
-        '''Returns a URL that can be used to serve a static collection
-        of bytes.
-
-        Args:
-          mime_type: MIME type for the data
-          data: Data to serve
-          expiration_secs: If not None, the number of seconds in which
-            the data will expire.
-        '''
-        return factory.get_state_instance().url_for_data(
-            mime_type, data, expiration)
-
-    def run(self):
-        '''Runs the test UI, waiting until the test completes.'''
-        event = self.event_client.wait(
-            lambda event:
-                (event.type == Event.Type.END_TEST and
-                 event.invocation == self.invocation and
-                 event.test == self.test))
-        logging.info('Received end test event %r', event)
-        self.event_client.close()
-
-        if event.status == TestState.PASSED:
-            pass
-        elif event.status == TestState.FAILED:
-            raise FactoryTestFailure(event.error_msg)
+    # Autoload .html and .js files.
+    for extension in ('js', 'html'):
+      autoload = filter(os.path.exists,
+               [x + '.' + extension
+                for x in autoload_bases])
+      if len(autoload) > 1:
+        raise FactoryTestFailure(
+          'Cannot have both of %s - delete one!' %
+          autoload)
+      if autoload:
+        factory.get_state_instance().register_path(
+          '/tests/%s/%s' % (self.test, os.path.basename(autoload[0])),
+          autoload[0])
+        if extension == 'html':
+          self.append_html(open(autoload[0]).read())
         else:
-            raise ValueError('Unexpected status in event %r' % event)
+          self.append_html('<script src="%s"></script>' %
+                   os.path.basename(autoload[0]))
 
-    def _handle_event(self, event):
-        '''Handles an event sent by a test UI.'''
-        if (event.type == Event.Type.TEST_UI_EVENT and
-            event.test == self.test and
-            event.invocation == self.invocation):
-            with self.lock:
-                for handler in self.event_handlers.get(event.subtype, []):
-                    handler(event)
+  def set_html(self, html, append=False):
+    '''Sets the UI in the test pane.'''
+    self.event_client.post_event(Event(Event.Type.SET_HTML,
+                      test=self.test,
+                      invocation=self.invocation,
+                      html=html,
+                      append=append))
+
+  def append_html(self, html):
+    '''Append to the UI in the test pane.'''
+    self.set_html(html, True)
+
+  def run_js(self, js, **kwargs):
+    '''Runs JavaScript code in the UI.
+
+    Args:
+      js: The JavaScript code to execute.
+      kwargs: Arguments to pass to the code; they will be
+        available in an "args" dict within the evaluation
+        context.
+
+    Example:
+      ui.run_js('alert(args.msg)', msg='The British are coming')
+    '''
+    self.event_client.post_event(Event(Event.Type.RUN_JS,
+                      test=self.test,
+                      invocation=self.invocation,
+                      js=js, args=kwargs))
+
+  def call_js_function(self, name, *args):
+    '''Calls a JavaScript function in the test pane.
+
+    This will be run within window scope (i.e., 'this' will be the
+    test pane window).
+
+    Args:
+      name: The name of the function to execute.
+      args: Arguments to the function.
+    '''
+    self.event_client.post_event(Event(Event.Type.CALL_JS_FUNCTION,
+                      test=self.test,
+                      invocation=self.invocation,
+                      name=name, args=args))
+
+  def add_event_handler(self, subtype, handler):
+    '''Adds an event handler.
+
+    Args:
+      subtype: The test-specific type of event to be handled.
+      handler: The handler to invoke with a single argument (the event
+        object).
+    '''
+    self.event_handlers.setdefault(subtype, []).append(handler)
+
+  def url_for_file(self, path):
+    '''Returns a URL that can be used to serve a local file.
+
+    Args:
+     path: path to the local file
+
+    Returns:
+     url: A (possibly relative) URL that refers to the file
+    '''
+    return factory.get_state_instance().url_for_file(path)
+
+  def url_for_data(self, mime_type, data, expiration=None):
+    '''Returns a URL that can be used to serve a static collection
+    of bytes.
+
+    Args:
+     mime_type: MIME type for the data
+     data: Data to serve
+     expiration_secs: If not None, the number of seconds in which
+      the data will expire.
+    '''
+    return factory.get_state_instance().url_for_data(
+      mime_type, data, expiration)
+
+  def run(self):
+    '''Runs the test UI, waiting until the test completes.'''
+    event = self.event_client.wait(
+      lambda event:
+        (event.type == Event.Type.END_TEST and
+         event.invocation == self.invocation and
+         event.test == self.test))
+    logging.info('Received end test event %r', event)
+    self.event_client.close()
+
+    if event.status == TestState.PASSED:
+      pass
+    elif event.status == TestState.FAILED:
+      raise FactoryTestFailure(event.error_msg)
+    else:
+      raise ValueError('Unexpected status in event %r' % event)
+
+  def _handle_event(self, event):
+    '''Handles an event sent by a test UI.'''
+    if (event.type == Event.Type.TEST_UI_EVENT and
+      event.test == self.test and
+      event.invocation == self.invocation):
+      with self.lock:
+        for handler in self.event_handlers.get(event.subtype, []):
+          handler(event)
diff --git a/py/test/ui.py b/py/test/ui.py
index 729f18e..a3c4ef7 100755
--- a/py/test/ui.py
+++ b/py/test/ui.py
@@ -19,12 +19,12 @@
 #
 # In short, the UI is composed of a 'console' panel on the bottom of
 # the screen which displays the autotest log, and there is also a
-# 'test list' panel on the right hand side of the screen.  The
+# 'test list' panel on the right hand side of the screen. The
 # majority of the screen is dedicated to tests, which are executed in
 # seperate processes, but instructed to display their own UIs in this
-# dedicated area whenever possible.  Tests in the test list are
+# dedicated area whenever possible. Tests in the test list are
 # executed in order by default, but can be activated on demand via
-# associated keyboard shortcuts.  As tests are run, their status is
+# associated keyboard shortcuts. As tests are run, their status is
 # color-indicated to the operator -- greyed out means untested, yellow
 # means active, green passed and red failed.
 
@@ -45,16 +45,16 @@
 import pango
 
 # Guard loading Xlib because it is currently not available in the
-# image build process host-depends list.  Failure to load in
+# image build process host-depends list. Failure to load in
 # production should always manifest during regular use.
 try:
-    from Xlib import X
-    from Xlib.display import Display
+  from Xlib import X
+  from Xlib.display import Display
 except:
-    pass
+  pass
 
 # Factory and autotest modules
-import factory_common
+import factory_common # pylint: disable=W0611
 from cros.factory.test import factory
 from cros.factory.test.factory import TestState
 from cros.factory.test.test_ui import FactoryTestFailure
@@ -73,9 +73,9 @@
 
 # Color definition
 BLACK = gtk.gdk.Color()
-RED =   gtk.gdk.Color(0xFFFF, 0, 0)
+RED =  gtk.gdk.Color(0xFFFF, 0, 0)
 GREEN = gtk.gdk.Color(0, 0xFFFF, 0)
-BLUE =  gtk.gdk.Color(0, 0, 0xFFFF)
+BLUE = gtk.gdk.Color(0, 0, 0xFFFF)
 WHITE = gtk.gdk.Color(0xFFFF, 0xFFFF, 0xFFFF)
 LIGHT_GREEN = gtk.gdk.color_parse('light green')
 SEP_COLOR = gtk.gdk.color_parse('grey50')
@@ -85,10 +85,10 @@
 RGBA_RED_OVERLAY = (0.5, 0, 0, 0.6)
 
 LABEL_COLORS = {
-    TestState.ACTIVE: gtk.gdk.color_parse('light goldenrod'),
-    TestState.PASSED: gtk.gdk.color_parse('pale green'),
-    TestState.FAILED: gtk.gdk.color_parse('tomato'),
-    TestState.UNTESTED: gtk.gdk.color_parse('dark slate grey')}
+  TestState.ACTIVE: gtk.gdk.color_parse('light goldenrod'),
+  TestState.PASSED: gtk.gdk.color_parse('pale green'),
+  TestState.FAILED: gtk.gdk.color_parse('tomato'),
+  TestState.UNTESTED: gtk.gdk.color_parse('dark slate grey')}
 
 LABEL_FONT = pango.FontDescription('courier new condensed 16')
 LABEL_LARGE_FONT = pango.FontDescription('courier new condensed 24')
@@ -96,21 +96,21 @@
 FAIL_TIMEOUT = 60
 
 MESSAGE_NO_ACTIVE_TESTS = (
-        "No more tests to run. To re-run items, press shortcuts\n"
-        "from the test list in right side or from following list:\n\n"
-        "Ctrl-Alt-A (Auto-Run):\n"
-        "  Test remaining untested items.\n\n"
-        "Ctrl-Alt-F (Re-run Failed):\n"
-        "  Re-test failed items.\n\n"
-        "Ctrl-Alt-R (Reset):\n"
-        "  Re-test everything.\n\n"
-        "Ctrl-Alt-Z (Information):\n"
-        "  Review test results and information.\n\n"
-        )
+    "No more tests to run. To re-run items, press shortcuts\n"
+    "from the test list in right side or from following list:\n\n"
+    "Ctrl-Alt-A (Auto-Run):\n"
+    " Test remaining untested items.\n\n"
+    "Ctrl-Alt-F (Re-run Failed):\n"
+    " Re-test failed items.\n\n"
+    "Ctrl-Alt-R (Reset):\n"
+    " Re-test everything.\n\n"
+    "Ctrl-Alt-Z (Information):\n"
+    " Review test results and information.\n\n"
+    )
 
 USER_PASS_FAIL_SELECT_STR = (
-    'hit TAB to fail and ENTER to pass\n' +
-    '錯誤請按 TAB,成功請按 ENTER')
+  'hit TAB to fail and ENTER to pass\n' +
+  '錯誤請按 TAB,成功請按 ENTER')
 # Resolution where original UI is designed for.
 _UI_SCREEN_WIDTH = 1280
 _UI_SCREEN_HEIGHT = 800
@@ -126,22 +126,22 @@
 _LABEL_TROUGH_COLOR = gtk.gdk.color_parse('grey20')
 _LABEL_STATUS_SIZE = (140, 30)
 _LABEL_STATUS_FONT = pango.FontDescription(
-    'courier new bold extra-condensed 16')
+  'courier new bold extra-condensed 16')
 _OTHER_LABEL_FONT = pango.FontDescription('courier new condensed 20')
 
 _NO_ACTIVE_TEST_DELAY_MS = 500
 
 GLOBAL_HOT_KEY_EVENTS = {
-    'r': Event.Type.RESTART_TESTS,
-    'a': Event.Type.AUTO_RUN,
-    'f': Event.Type.RE_RUN_FAILED,
-    'z': Event.Type.REVIEW,
-    }
+  'r': Event.Type.RESTART_TESTS,
+  'a': Event.Type.AUTO_RUN,
+  'f': Event.Type.RE_RUN_FAILED,
+  'z': Event.Type.REVIEW,
+  }
 try:
-    # Works only if X is available.
-    GLOBAL_HOT_KEY_MASK = X.ControlMask | X.Mod1Mask
+  # Works only if X is available.
+  GLOBAL_HOT_KEY_MASK = X.ControlMask | X.Mod1Mask
 except:
-    pass
+  pass
 
 # ---------------------------------------------------------------------------
 # Client Library
@@ -150,415 +150,415 @@
 # TODO(hungte) Replace gtk_lock by gtk.gdk.lock when it's availble (need pygtk
 # 2.2x, and we're now pinned by 2.1x)
 class _GtkLock(object):
-    __enter__ = gtk.gdk.threads_enter
-    def __exit__(*ignored):
-        gtk.gdk.threads_leave()
+  __enter__ = gtk.gdk.threads_enter
+  def __exit__(*ignored):
+    gtk.gdk.threads_leave()
 
 
 gtk_lock = _GtkLock()
 
 
 def make_label(message, font=LABEL_FONT, fg=LIGHT_GREEN,
-               size=None, alignment=None):
-    """Returns a label widget.
+        size=None, alignment=None):
+  """Returns a label widget.
 
-    A wrapper for gtk.Label. The unit of size is pixels under resolution
-    _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
+  A wrapper for gtk.Label. The unit of size is pixels under resolution
+  _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
 
-    @param message: A string to be displayed.
-    @param font: Font descriptor for the label.
-    @param fg: Foreground color.
-    @param size: Minimum size for this label.
-    @param alignment: Alignment setting.
-    @return: A label widget.
-    """
-    l = gtk.Label(message)
-    l.modify_font(font)
-    l.modify_fg(gtk.STATE_NORMAL, fg)
-    if size:
-        # Convert size according to the current resolution.
-        l.set_size_request(*convert_pixels(size))
-    if alignment:
-        l.set_alignment(*alignment)
-    return l
+  @param message: A string to be displayed.
+  @param font: Font descriptor for the label.
+  @param fg: Foreground color.
+  @param size: Minimum size for this label.
+  @param alignment: Alignment setting.
+  @return: A label widget.
+  """
+  l = gtk.Label(message)
+  l.modify_font(font)
+  l.modify_fg(gtk.STATE_NORMAL, fg)
+  if size:
+    # Convert size according to the current resolution.
+    l.set_size_request(*convert_pixels(size))
+  if alignment:
+    l.set_alignment(*alignment)
+  return l
 
 
 def make_status_row(init_prompt,
-                    init_status,
-                    label_size=_LABEL_STATUS_ROW_SIZE,
-                    is_standard_status=True):
-    """Returns a widget that live updates prompt and status in a row.
+          init_status,
+          label_size=_LABEL_STATUS_ROW_SIZE,
+          is_standard_status=True):
+  """Returns a widget that live updates prompt and status in a row.
 
-    Args:
-        init_prompt: The prompt label text.
-        init_status: The status label text.
-        label_size: The desired size of the prompt label and the status label.
-        is_standard_status: True to interpret status by the values defined by
-            LABEL_COLORS, and render text by corresponding color. False to
-            display arbitrary text without changing text color.
+  Args:
+    init_prompt: The prompt label text.
+    init_status: The status label text.
+    label_size: The desired size of the prompt label and the status label.
+    is_standard_status: True to interpret status by the values defined by
+      LABEL_COLORS, and render text by corresponding color. False to
+      display arbitrary text without changing text color.
 
-    Returns:
-        1) A dict whose content is linked by the widget.
-        2) A widget to render dict content in "prompt:   status" format.
-    """
-    display_dict = {}
-    display_dict['prompt'] = init_prompt
-    display_dict['status'] = init_status
-    display_dict['is_standard_status'] = is_standard_status
+  Returns:
+    1) A dict whose content is linked by the widget.
+    2) A widget to render dict content in "prompt:  status" format.
+  """
+  display_dict = {}
+  display_dict['prompt'] = init_prompt
+  display_dict['status'] = init_status
+  display_dict['is_standard_status'] = is_standard_status
 
-    def prompt_label_expose(widget, event):
-        prompt = display_dict['prompt']
-        widget.set_text(prompt)
+  def prompt_label_expose(widget, event):
+    prompt = display_dict['prompt']
+    widget.set_text(prompt)
 
-    def status_label_expose(widget, event):
-        status = display_dict['status']
-        widget.set_text(status)
-        if is_standard_status:
-            widget.modify_fg(gtk.STATE_NORMAL, LABEL_COLORS[status])
+  def status_label_expose(widget, event):
+    status = display_dict['status']
+    widget.set_text(status)
+    if is_standard_status:
+      widget.modify_fg(gtk.STATE_NORMAL, LABEL_COLORS[status])
 
-    prompt_label = make_label(
-            init_prompt, size=label_size,
-            alignment=(0, 0.5))
-    delimiter_label = make_label(':', alignment=(0, 0.5))
-    status_label = make_label(
-            init_status, size=label_size,
-            alignment=(0, 0.5))
+  prompt_label = make_label(
+      init_prompt, size=label_size,
+      alignment=(0, 0.5))
+  delimiter_label = make_label(':', alignment=(0, 0.5))
+  status_label = make_label(
+      init_status, size=label_size,
+      alignment=(0, 0.5))
 
-    widget = gtk.HBox()
-    widget.pack_end(status_label, False, False)
-    widget.pack_end(delimiter_label, False, False)
-    widget.pack_end(prompt_label, False, False)
+  widget = gtk.HBox()
+  widget.pack_end(status_label, False, False)
+  widget.pack_end(delimiter_label, False, False)
+  widget.pack_end(prompt_label, False, False)
 
-    status_label.connect('expose_event', status_label_expose)
-    prompt_label.connect('expose_event', prompt_label_expose)
-    return display_dict, widget
+  status_label.connect('expose_event', status_label_expose)
+  prompt_label.connect('expose_event', prompt_label_expose)
+  return display_dict, widget
 
 
 def convert_pixels(size):
-    """Converts a pair in pixel that is suitable for current resolution.
+  """Converts a pair in pixel that is suitable for current resolution.
 
-    GTK takes pixels as its unit in many function calls. To maintain the
-    consistency of the UI in different resolution, a conversion is required.
-    Take current resolution and (_UI_SCREEN_WIDTH, _UI_SCREEN_HEIGHT) as
-    the original resolution, this function returns a pair of width and height
-    that is converted for current resolution.
+  GTK takes pixels as its unit in many function calls. To maintain the
+  consistency of the UI in different resolution, a conversion is required.
+  Take current resolution and (_UI_SCREEN_WIDTH, _UI_SCREEN_HEIGHT) as
+  the original resolution, this function returns a pair of width and height
+  that is converted for current resolution.
 
-    Because pixels in negative usually indicates unspecified, no conversion
-    will be done for negative pixels.
+  Because pixels in negative usually indicates unspecified, no conversion
+  will be done for negative pixels.
 
-    In addition, the aspect ratio is not maintained in this function.
+  In addition, the aspect ratio is not maintained in this function.
 
-    Usage Example:
-        width,_ = convert_pixels((20,-1))
+  Usage Example:
+    width,_ = convert_pixels((20,-1))
 
-    @param size: A pair of pixels that designed under original resolution.
-    @return: A pair of pixels of (width, height) format.
-             Pixels returned are always integer.
-    """
-    return (int(float(size[0]) / _UI_SCREEN_WIDTH * gtk.gdk.screen_width()
-           if (size[0] > 0) else size[0]),
-           int(float(size[1]) / _UI_SCREEN_HEIGHT * gtk.gdk.screen_height()
-           if (size[1] > 0) else size[1]))
+  @param size: A pair of pixels that designed under original resolution.
+  @return: A pair of pixels of (width, height) format.
+       Pixels returned are always integer.
+  """
+  return (int(float(size[0]) / _UI_SCREEN_WIDTH * gtk.gdk.screen_width()
+      if (size[0] > 0) else size[0]),
+      int(float(size[1]) / _UI_SCREEN_HEIGHT * gtk.gdk.screen_height()
+      if (size[1] > 0) else size[1]))
 
 
 def make_hsep(height=1):
-    """Returns a widget acts as a horizontal separation line.
+  """Returns a widget acts as a horizontal separation line.
 
-    The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
-    """
-    frame = gtk.EventBox()
-    # Convert height according to the current resolution.
-    frame.set_size_request(*convert_pixels((-1, height)))
-    frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
-    return frame
+  The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
+  """
+  frame = gtk.EventBox()
+  # Convert height according to the current resolution.
+  frame.set_size_request(*convert_pixels((-1, height)))
+  frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
+  return frame
 
 
 def make_vsep(width=1):
-    """Returns a widget acts as a vertical separation line.
+  """Returns a widget acts as a vertical separation line.
 
-    The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
-    """
-    frame = gtk.EventBox()
-    # Convert width according to the current resolution.
-    frame.set_size_request(*convert_pixels((width, -1)))
-    frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
-    return frame
+  The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
+  """
+  frame = gtk.EventBox()
+  # Convert width according to the current resolution.
+  frame.set_size_request(*convert_pixels((width, -1)))
+  frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
+  return frame
 
 
 def make_countdown_widget(prompt=None, value=None, fg=LIGHT_GREEN):
-    if prompt is None:
-        prompt = 'time remaining / 剩餘時間: '
-    if value is None:
-        value = '%s' % FAIL_TIMEOUT
-    title = make_label(prompt, fg=fg, alignment=(1, 0.5))
-    countdown = make_label(value, fg=fg, alignment=(0, 0.5))
-    hbox = gtk.HBox()
-    hbox.pack_start(title)
-    hbox.pack_start(countdown)
-    eb = gtk.EventBox()
-    eb.modify_bg(gtk.STATE_NORMAL, BLACK)
-    eb.add(hbox)
-    return eb, countdown
+  if prompt is None:
+    prompt = 'time remaining / 剩餘時間: '
+  if value is None:
+    value = '%s' % FAIL_TIMEOUT
+  title = make_label(prompt, fg=fg, alignment=(1, 0.5))
+  countdown = make_label(value, fg=fg, alignment=(0, 0.5))
+  hbox = gtk.HBox()
+  hbox.pack_start(title)
+  hbox.pack_start(countdown)
+  eb = gtk.EventBox()
+  eb.modify_bg(gtk.STATE_NORMAL, BLACK)
+  eb.add(hbox)
+  return eb, countdown
 
 
 def is_chrome_ui():
-    return os.environ.get('CROS_UI') == 'chrome'
+  return os.environ.get('CROS_UI') == 'chrome'
 
 
 def hide_cursor(gdk_window):
-    pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
-    color = gtk.gdk.Color()
-    cursor = gtk.gdk.Cursor(pixmap, pixmap, color, color, 0, 0)
-    gdk_window.set_cursor(cursor)
+  pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
+  color = gtk.gdk.Color()
+  cursor = gtk.gdk.Cursor(pixmap, pixmap, color, color, 0, 0)
+  gdk_window.set_cursor(cursor)
 
 
 def calc_scale(wanted_x, wanted_y):
-    (widget_size_x, widget_size_y) = factory.get_shared_data('test_widget_size')
-    scale_x = (0.9 * widget_size_x) / wanted_x
-    scale_y = (0.9 * widget_size_y) / wanted_y
-    scale = scale_y if scale_y < scale_x else scale_x
-    scale = 1 if scale > 1 else scale
-    factory.log('scale: %s' % scale)
-    return scale
+  (widget_size_x, widget_size_y) = factory.get_shared_data('test_widget_size')
+  scale_x = (0.9 * widget_size_x) / wanted_x
+  scale_y = (0.9 * widget_size_y) / wanted_y
+  scale = scale_y if scale_y < scale_x else scale_x
+  scale = 1 if scale > 1 else scale
+  factory.log('scale: %s' % scale)
+  return scale
 
 
 def trim(text, length):
-    if len(text) > length:
-        text = text[:length-3] + '...'
-    return text
+  if len(text) > length:
+    text = text[:length-3] + '...'
+  return text
 
 
 class InputError(ValueError):
-    """Execption for input window callbacks to change status text message."""
-    pass
+  """Execption for input window callbacks to change status text message."""
+  pass
 
 
 def make_input_window(prompt=None,
-                      init_value=None,
-                      msg_invalid=None,
-                      font=None,
-                      on_validate=None,
-                      on_keypress=None,
-                      on_complete=None):
-    """Creates a widget to prompt user for a valid string.
+           init_value=None,
+           msg_invalid=None,
+           font=None,
+           on_validate=None,
+           on_keypress=None,
+           on_complete=None):
+  """Creates a widget to prompt user for a valid string.
 
-    @param prompt: A string to be displayed. None for default message.
-    @param init_value: Initial value to be set.
-    @param msg_invalid: Status string to display when input is invalid. None for
-        default message.
-    @param font: Font specification (string or pango.FontDescription) for label
-        and entry. None for default large font.
-    @param on_validate: A callback function to validate if the input from user
-        is valid. None for allowing any non-empty input. Any ValueError or
-        ui.InputError raised during execution in on_validate will be displayed
-        in bottom status.
-    @param on_keypress: A callback function when each keystroke is hit.
-    @param on_complete: A callback function when a valid string is passed.
-        None to stop (gtk.main_quit).
-    @return: A widget with prompt, input entry, and status label. To access
-        these elements, use attribute 'prompt', 'entry', and 'label'.
-    """
-    DEFAULT_MSG_INVALID = "Invalid input / 輸入不正確"
-    DEFAULT_PROMPT = "Enter Data / 輸入資料:"
+  @param prompt: A string to be displayed. None for default message.
+  @param init_value: Initial value to be set.
+  @param msg_invalid: Status string to display when input is invalid. None for
+    default message.
+  @param font: Font specification (string or pango.FontDescription) for label
+    and entry. None for default large font.
+  @param on_validate: A callback function to validate if the input from user
+    is valid. None for allowing any non-empty input. Any ValueError or
+    ui.InputError raised during execution in on_validate will be displayed
+    in bottom status.
+  @param on_keypress: A callback function when each keystroke is hit.
+  @param on_complete: A callback function when a valid string is passed.
+    None to stop (gtk.main_quit).
+  @return: A widget with prompt, input entry, and status label. To access
+    these elements, use attribute 'prompt', 'entry', and 'label'.
+  """
+  DEFAULT_MSG_INVALID = "Invalid input / 輸入不正確"
+  DEFAULT_PROMPT = "Enter Data / 輸入資料:"
 
-    def enter_callback(entry):
-        text = entry.get_text()
-        try:
-            if (on_validate and (not on_validate(text))) or (not text.strip()):
-                raise ValueError(msg_invalid)
-            on_complete(text) if on_complete else gtk.main_quit()
-        except ValueError as e:
-            gtk.gdk.beep()
-            status_label.set_text('ERROR: %s' % e.message)
-        return True
+  def enter_callback(entry):
+    text = entry.get_text()
+    try:
+      if (on_validate and (not on_validate(text))) or (not text.strip()):
+        raise ValueError(msg_invalid)
+      on_complete(text) if on_complete else gtk.main_quit()
+    except ValueError as e:
+      gtk.gdk.beep()
+      status_label.set_text('ERROR: %s' % e.message)
+    return True
 
-    def key_press_callback(entry, key):
-        status_label.set_text('')
-        if on_keypress:
-            return on_keypress(entry, key)
-        return False
+  def key_press_callback(entry, key):
+    status_label.set_text('')
+    if on_keypress:
+      return on_keypress(entry, key)
+    return False
 
-    # Populate default parameters
-    if msg_invalid is None:
-        msg_invalid = DEFAULT_MSG_INVALID
+  # Populate default parameters
+  if msg_invalid is None:
+    msg_invalid = DEFAULT_MSG_INVALID
 
-    if prompt is None:
-        prompt = DEFAULT_PROMPT
+  if prompt is None:
+    prompt = DEFAULT_PROMPT
 
-    if font is None:
-        font = LABEL_LARGE_FONT
-    elif not isinstance(font, pango.FontDescription):
-        font = pango.FontDescription(font)
+  if font is None:
+    font = LABEL_LARGE_FONT
+  elif not isinstance(font, pango.FontDescription):
+    font = pango.FontDescription(font)
 
-    widget = gtk.VBox()
-    label = make_label(prompt, font=font)
-    status_label = make_label('', font=font)
-    entry = gtk.Entry()
-    entry.modify_font(font)
-    entry.connect("activate", enter_callback)
-    entry.connect("key_press_event", key_press_callback)
-    if init_value:
-        entry.set_text(init_value)
-    widget.modify_bg(gtk.STATE_NORMAL, BLACK)
-    status_label.modify_fg(gtk.STATE_NORMAL, RED)
-    widget.add(label)
-    widget.pack_start(entry)
-    widget.pack_start(status_label)
+  widget = gtk.VBox()
+  label = make_label(prompt, font=font)
+  status_label = make_label('', font=font)
+  entry = gtk.Entry()
+  entry.modify_font(font)
+  entry.connect("activate", enter_callback)
+  entry.connect("key_press_event", key_press_callback)
+  if init_value:
+    entry.set_text(init_value)
+  widget.modify_bg(gtk.STATE_NORMAL, BLACK)
+  status_label.modify_fg(gtk.STATE_NORMAL, RED)
+  widget.add(label)
+  widget.pack_start(entry)
+  widget.pack_start(status_label)
 
-    widget.entry = entry
-    widget.status = status_label
-    widget.prompt = label
+  widget.entry = entry
+  widget.status = status_label
+  widget.prompt = label
 
-    # TODO(itspeter) Replace deprecated get_entry by widget.entry.
-    # Method for getting the entry.
-    widget.get_entry = lambda : entry
-    return widget
+  # TODO(itspeter) Replace deprecated get_entry by widget.entry.
+  # Method for getting the entry.
+  widget.get_entry = lambda : entry
+  return widget
 
 
 def make_summary_box(tests, state_map, rows=15):
-    '''Creates a widget display status of a set of test.
+  '''Creates a widget display status of a set of test.
 
-    @param tests: A list of FactoryTest nodes whose status (and children's
-        status) should be displayed.
-    @param state_map: The state map as provide by the state instance.
-    @param rows: The number of rows to display.
-    @return: A tuple (widget, label_map), where widget is the widget, and
-        label_map is a map from each test to the corresponding label.
-    '''
-    LABEL_EN_SIZE = (170, 35)
-    LABEL_EN_SIZE_2 = (450, 25)
-    LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
+  @param tests: A list of FactoryTest nodes whose status (and children's
+    status) should be displayed.
+  @param state_map: The state map as provide by the state instance.
+  @param rows: The number of rows to display.
+  @return: A tuple (widget, label_map), where widget is the widget, and
+    label_map is a map from each test to the corresponding label.
+  '''
+  LABEL_EN_SIZE = (170, 35)
+  LABEL_EN_SIZE_2 = (450, 25)
+  LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
 
-    all_tests = sum([list(t.walk(in_order=True)) for t in tests], [])
-    columns = len(all_tests) / rows + (len(all_tests) % rows != 0)
+  all_tests = sum([list(t.walk(in_order=True)) for t in tests], [])
+  columns = len(all_tests) / rows + (len(all_tests) % rows != 0)
 
-    info_box = gtk.HBox()
-    info_box.set_spacing(20)
-    for status in (TestState.ACTIVE, TestState.PASSED,
-                   TestState.FAILED, TestState.UNTESTED):
-        label = make_label(status,
-                               size=LABEL_EN_SIZE,
-                               font=LABEL_EN_FONT,
-                               alignment=(0.5, 0.5),
-                               fg=LABEL_COLORS[status])
-        info_box.pack_start(label, False, False)
+  info_box = gtk.HBox()
+  info_box.set_spacing(20)
+  for status in (TestState.ACTIVE, TestState.PASSED,
+          TestState.FAILED, TestState.UNTESTED):
+    label = make_label(status,
+                size=LABEL_EN_SIZE,
+                font=LABEL_EN_FONT,
+                alignment=(0.5, 0.5),
+                fg=LABEL_COLORS[status])
+    info_box.pack_start(label, False, False)
 
-    vbox = gtk.VBox()
-    vbox.set_spacing(20)
-    vbox.pack_start(info_box, False, False)
+  vbox = gtk.VBox()
+  vbox.set_spacing(20)
+  vbox.pack_start(info_box, False, False)
 
-    label_map = {}
+  label_map = {}
 
-    if all_tests:
-        status_table = gtk.Table(rows, columns, True)
-        for (j, i), t in izip(product(xrange(columns), xrange(rows)),
-                              all_tests):
-            msg_en = '  ' * (t.depth() - 1) + t.label_en
-            msg_en = trim(msg_en, 12)
-            if t.label_zh:
-                msg = '{0:<12} ({1})'.format(msg_en, t.label_zh)
-            else:
-                msg = msg_en
-            status = state_map[t].status
-            status_label = make_label(msg,
-                                      size=LABEL_EN_SIZE_2,
-                                      font=LABEL_EN_FONT,
-                                      alignment=(0.0, 0.5),
-                                      fg=LABEL_COLORS[status])
-            label_map[t] = status_label
-            status_table.attach(status_label, j, j+1, i, i+1)
-        vbox.pack_start(status_table, False, False)
+  if all_tests:
+    status_table = gtk.Table(rows, columns, True)
+    for (j, i), t in izip(product(xrange(columns), xrange(rows)),
+               all_tests):
+      msg_en = ' ' * (t.depth() - 1) + t.label_en
+      msg_en = trim(msg_en, 12)
+      if t.label_zh:
+        msg = '{0:<12} ({1})'.format(msg_en, t.label_zh)
+      else:
+        msg = msg_en
+      status = state_map[t].status
+      status_label = make_label(msg,
+                   size=LABEL_EN_SIZE_2,
+                   font=LABEL_EN_FONT,
+                   alignment=(0.0, 0.5),
+                   fg=LABEL_COLORS[status])
+      label_map[t] = status_label
+      status_table.attach(status_label, j, j+1, i, i+1)
+    vbox.pack_start(status_table, False, False)
 
-    return vbox, label_map
+  return vbox, label_map
 
 
 def run_test_widget(dummy_job, test_widget,
-                    invisible_cursor=True,
-                    window_registration_callback=None,
-                    cleanup_callback=None):
-    test_widget_size = factory.get_shared_data('test_widget_size')
+          invisible_cursor=True,
+          window_registration_callback=None,
+          cleanup_callback=None):
+  test_widget_size = factory.get_shared_data('test_widget_size')
 
-    window = gtk.Window(gtk.WINDOW_TOPLEVEL)
-    window.modify_bg(gtk.STATE_NORMAL, BLACK)
-    window.set_size_request(*test_widget_size)
+  window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+  window.modify_bg(gtk.STATE_NORMAL, BLACK)
+  window.set_size_request(*test_widget_size)
 
-    test_widget_position = factory.get_shared_data('test_widget_position')
-    if test_widget_position:
-        window.move(*test_widget_position)
+  test_widget_position = factory.get_shared_data('test_widget_position')
+  if test_widget_position:
+    window.move(*test_widget_position)
 
-    def show_window():
-        window.show()
-        window.window.raise_()  # pylint: disable=E1101
-        if is_chrome_ui():
-            window.present()
-            window.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
-        else:
-            gtk.gdk.pointer_grab(window.window, confine_to=window.window)
-            if invisible_cursor:
-                hide_cursor(window.window)
-
-    test_path = factory.get_current_test_path()
-
-    def handle_event(event):
-        if (event.type == Event.Type.STATE_CHANGE and
-            test_path and event.path == test_path and
-            event.state.visible):
-            show_window()
-
-    event_client = EventClient(
-            callback=handle_event, event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
-
-    align = gtk.Alignment(xalign=0.5, yalign=0.5)
-    align.add(test_widget)
-
-    window.add(align)
-    for c in window.get_children():
-        # Show all children, but not the window itself yet.
-        c.show_all()
-
-    if window_registration_callback is not None:
-        window_registration_callback(window)
-
-    # Show the window if it is the visible test, or if the test_path is not
-    # available (e.g., run directly from the command line).
-    if (not test_path) or (
-        TestState.from_dict_or_object(
-            factory.get_state_instance().get_test_state(test_path)).visible):
-        show_window()
+  def show_window():
+    window.show()
+    window.window.raise_() # pylint: disable=E1101
+    if is_chrome_ui():
+      window.present()
+      window.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
     else:
-        window.hide()
+      gtk.gdk.pointer_grab(window.window, confine_to=window.window)
+      if invisible_cursor:
+        hide_cursor(window.window)
 
-    # When gtk.main() is running, it ignores all uncaught exceptions, which is
-    # not preferred by most of our factory tests.  To prevent writing special
-    # function raising errors, we hook top level exception handler to always
-    # leave GTK main and raise exception again.
+  test_path = factory.get_current_test_path()
 
-    def exception_hook(exc_type, value, traceback):
-       # Prevent re-entrant.
-       sys.excepthook = old_excepthook
-       session['exception'] = (exc_type, value, traceback)
-       gobject.idle_add(gtk.main_quit)
-       return old_excepthook(exc_type, value, traceback)
+  def handle_event(event):
+    if (event.type == Event.Type.STATE_CHANGE and
+      test_path and event.path == test_path and
+      event.state.visible):
+      show_window()
 
-    session = {}
-    old_excepthook = sys.excepthook
-    sys.excepthook = exception_hook
+  event_client = EventClient(
+      callback=handle_event, event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
 
-    gtk.main()
+  align = gtk.Alignment(xalign=0.5, yalign=0.5)
+  align.add(test_widget)
 
-    if not is_chrome_ui():
-        gtk.gdk.pointer_ungrab()
+  window.add(align)
+  for c in window.get_children():
+    # Show all children, but not the window itself yet.
+    c.show_all()
 
-    if cleanup_callback is not None:
-        cleanup_callback()
+  if window_registration_callback is not None:
+    window_registration_callback(window)
 
-    del event_client
+  # Show the window if it is the visible test, or if the test_path is not
+  # available (e.g., run directly from the command line).
+  if (not test_path) or (
+    TestState.from_dict_or_object(
+      factory.get_state_instance().get_test_state(test_path)).visible):
+    show_window()
+  else:
+    window.hide()
 
+  # When gtk.main() is running, it ignores all uncaught exceptions, which is
+  # not preferred by most of our factory tests. To prevent writing special
+  # function raising errors, we hook top level exception handler to always
+  # leave GTK main and raise exception again.
+
+  def exception_hook(exc_type, value, traceback):
+    # Prevent re-entrant.
     sys.excepthook = old_excepthook
-    exc_info = session.get('exception')
-    if exc_info is not None:
-       logging.error(exc_info[0], exc_info=exc_info)
-       raise FactoryTestFailure(exc_info[1])
+    session['exception'] = (exc_type, value, traceback)
+    gobject.idle_add(gtk.main_quit)
+    return old_excepthook(exc_type, value, traceback)
+
+  session = {}
+  old_excepthook = sys.excepthook
+  sys.excepthook = exception_hook
+
+  gtk.main()
+
+  if not is_chrome_ui():
+    gtk.gdk.pointer_ungrab()
+
+  if cleanup_callback is not None:
+    cleanup_callback()
+
+  del event_client
+
+  sys.excepthook = old_excepthook
+  exc_info = session.get('exception')
+  if exc_info is not None:
+    logging.error(exc_info[0], exc_info=exc_info)
+    raise FactoryTestFailure(exc_info[1])
 
 
 
@@ -567,607 +567,607 @@
 
 
 class Console(object):
-    '''Display a progress log.  Implemented by launching an borderless
-    xterm at a strategic location, and running tail against the log.'''
+  '''Display a progress log. Implemented by launching an borderless
+  xterm at a strategic location, and running tail against the log.'''
 
-    def __init__(self, allocation):
-        # Specify how many lines and characters per line are displayed.
-        XTERM_DISPLAY_LINES = 13
-        XTERM_DISPLAY_CHARS = 120
-        # Extra space reserved for pixels between lines.
-        XTERM_RESERVED_LINES = 3
+  def __init__(self, allocation):
+    # Specify how many lines and characters per line are displayed.
+    XTERM_DISPLAY_LINES = 13
+    XTERM_DISPLAY_CHARS = 120
+    # Extra space reserved for pixels between lines.
+    XTERM_RESERVED_LINES = 3
 
-        xterm_coords = '%dx%d+%d+%d' % (XTERM_DISPLAY_CHARS,
-                                        XTERM_DISPLAY_LINES,
-                                        allocation.x,
-                                        allocation.y)
-        xterm_reserved_height = gtk.gdk.screen_height() - allocation.y
-        font_size = int(float(xterm_reserved_height) / (XTERM_DISPLAY_LINES +
-                                                        XTERM_RESERVED_LINES))
-        logging.info('xterm_reserved_height = %d' % xterm_reserved_height)
-        logging.info('font_size = %d' % font_size)
-        logging.info('xterm_coords = %s', xterm_coords)
-        xterm_opts = ('-bg black -fg lightgray -bw 0 -g %s' % xterm_coords)
-        xterm_cmd = (
-            ['urxvt'] + xterm_opts.split() +
-            ['-fn', 'xft:DejaVu Sans Mono:pixelsize=%s' % font_size] +
-            ['-e', 'bash'] +
-            ['-c', 'tail -f "%s"' % factory.CONSOLE_LOG_PATH])
-        logging.info('xterm_cmd = %s', xterm_cmd)
-        self._proc = subprocess.Popen(xterm_cmd)
+    xterm_coords = '%dx%d+%d+%d' % (XTERM_DISPLAY_CHARS,
+                    XTERM_DISPLAY_LINES,
+                    allocation.x,
+                    allocation.y)
+    xterm_reserved_height = gtk.gdk.screen_height() - allocation.y
+    font_size = int(float(xterm_reserved_height) / (XTERM_DISPLAY_LINES +
+                            XTERM_RESERVED_LINES))
+    logging.info('xterm_reserved_height = %d' % xterm_reserved_height)
+    logging.info('font_size = %d' % font_size)
+    logging.info('xterm_coords = %s', xterm_coords)
+    xterm_opts = ('-bg black -fg lightgray -bw 0 -g %s' % xterm_coords)
+    xterm_cmd = (
+      ['urxvt'] + xterm_opts.split() +
+      ['-fn', 'xft:DejaVu Sans Mono:pixelsize=%s' % font_size] +
+      ['-e', 'bash'] +
+      ['-c', 'tail -f "%s"' % factory.CONSOLE_LOG_PATH])
+    logging.info('xterm_cmd = %s', xterm_cmd)
+    self._proc = subprocess.Popen(xterm_cmd)
 
-    def __del__(self):
-        logging.info('console_proc __del__')
-        self._proc.kill()
+  def __del__(self):
+    logging.info('console_proc __del__')
+    self._proc.kill()
 
 
-class TestLabelBox(gtk.EventBox):  # pylint: disable=R0904
+class TestLabelBox(gtk.EventBox): # pylint: disable=R0904
 
-    def __init__(self, test):
-        gtk.EventBox.__init__(self)
-        self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[TestState.UNTESTED])
-        self._is_group = test.is_group()
-        depth = len(test.get_ancestor_groups())
-        self._label_text = ' %s%s%s' % (
-                ' ' * depth,
-                SYMBOL_RIGHT_ARROW if self._is_group else ' ',
-                test.label_en)
-        if self._is_group:
-            self._label_text_collapsed = ' %s%s%s' % (
-                    ' ' * depth,
-                    SYMBOL_DOWN_ARROW if self._is_group else '',
-                    test.label_en)
-        self._label_en = make_label(
-            self._label_text, size=_LABEL_EN_SIZE,
-            font=_LABEL_EN_FONT, alignment=(0, 0.5),
-            fg=_LABEL_UNTESTED_FG)
-        self._label_zh = make_label(
-            test.label_zh, size=_LABEL_ZH_SIZE,
-            font=_LABEL_ZH_FONT, alignment=(0.5, 0.5),
-            fg=_LABEL_UNTESTED_FG)
-        self._label_t = make_label(
-            '', size=_LABEL_T_SIZE, font=_LABEL_T_FONT,
-            alignment=(0.5, 0.5), fg=BLACK)
-        hbox = gtk.HBox()
-        hbox.pack_start(self._label_en, False, False)
-        hbox.pack_start(self._label_zh, False, False)
-        hbox.pack_start(self._label_t, False, False)
-        vbox = gtk.VBox()
-        vbox.pack_start(hbox, False, False)
-        vbox.pack_start(make_hsep(), False, False)
-        self.add(vbox)
-        self._status = None
+  def __init__(self, test):
+    gtk.EventBox.__init__(self)
+    self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[TestState.UNTESTED])
+    self._is_group = test.is_group()
+    depth = len(test.get_ancestor_groups())
+    self._label_text = ' %s%s%s' % (
+        ' ' * depth,
+        SYMBOL_RIGHT_ARROW if self._is_group else ' ',
+        test.label_en)
+    if self._is_group:
+      self._label_text_collapsed = ' %s%s%s' % (
+          ' ' * depth,
+          SYMBOL_DOWN_ARROW if self._is_group else '',
+          test.label_en)
+    self._label_en = make_label(
+      self._label_text, size=_LABEL_EN_SIZE,
+      font=_LABEL_EN_FONT, alignment=(0, 0.5),
+      fg=_LABEL_UNTESTED_FG)
+    self._label_zh = make_label(
+      test.label_zh, size=_LABEL_ZH_SIZE,
+      font=_LABEL_ZH_FONT, alignment=(0.5, 0.5),
+      fg=_LABEL_UNTESTED_FG)
+    self._label_t = make_label(
+      '', size=_LABEL_T_SIZE, font=_LABEL_T_FONT,
+      alignment=(0.5, 0.5), fg=BLACK)
+    hbox = gtk.HBox()
+    hbox.pack_start(self._label_en, False, False)
+    hbox.pack_start(self._label_zh, False, False)
+    hbox.pack_start(self._label_t, False, False)
+    vbox = gtk.VBox()
+    vbox.pack_start(hbox, False, False)
+    vbox.pack_start(make_hsep(), False, False)
+    self.add(vbox)
+    self._status = None
 
-    def set_shortcut(self, shortcut):
-        if shortcut is None:
-            return
-        self._label_t.set_text('C-%s' % shortcut.upper())
-        attrs = self._label_en.get_attributes() or pango.AttrList()
-        attrs.filter(lambda attr: attr.type == pango.ATTR_UNDERLINE)
-        index_hotkey = self._label_en.get_text().upper().find(shortcut.upper())
-        if index_hotkey != -1:
-            attrs.insert(pango.AttrUnderline(
-                pango.UNDERLINE_LOW, index_hotkey, index_hotkey + 1))
-            attrs.insert(pango.AttrWeight(
-                pango.WEIGHT_BOLD, index_hotkey, index_hotkey + 1))
-        self._label_en.set_attributes(attrs)
-        self.queue_draw()
+  def set_shortcut(self, shortcut):
+    if shortcut is None:
+      return
+    self._label_t.set_text('C-%s' % shortcut.upper())
+    attrs = self._label_en.get_attributes() or pango.AttrList()
+    attrs.filter(lambda attr: attr.type == pango.ATTR_UNDERLINE)
+    index_hotkey = self._label_en.get_text().upper().find(shortcut.upper())
+    if index_hotkey != -1:
+      attrs.insert(pango.AttrUnderline(
+        pango.UNDERLINE_LOW, index_hotkey, index_hotkey + 1))
+      attrs.insert(pango.AttrWeight(
+        pango.WEIGHT_BOLD, index_hotkey, index_hotkey + 1))
+    self._label_en.set_attributes(attrs)
+    self.queue_draw()
 
-    def update(self, status):
-        if self._status == status:
-            return
-        self._status = status
-        label_fg = (_LABEL_UNTESTED_FG if status == TestState.UNTESTED
-                    else BLACK)
-        if self._is_group:
-            self._label_en.set_text(
-                    self._label_text_collapsed if status == TestState.ACTIVE
-                    else self._label_text)
+  def update(self, status):
+    if self._status == status:
+      return
+    self._status = status
+    label_fg = (_LABEL_UNTESTED_FG if status == TestState.UNTESTED
+          else BLACK)
+    if self._is_group:
+      self._label_en.set_text(
+          self._label_text_collapsed if status == TestState.ACTIVE
+          else self._label_text)
 
-        for label in [self._label_en, self._label_zh, self._label_t]:
-            label.modify_fg(gtk.STATE_NORMAL, label_fg)
-        self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[status])
-        self.queue_draw()
+    for label in [self._label_en, self._label_zh, self._label_t]:
+      label.modify_fg(gtk.STATE_NORMAL, label_fg)
+    self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[status])
+    self.queue_draw()
 
 
 class ReviewInformation(object):
 
-    LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
-    TAB_BORDER = 20
+  LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
+  TAB_BORDER = 20
 
-    def __init__(self, test_list):
-        self.test_list = test_list
+  def __init__(self, test_list):
+    self.test_list = test_list
 
-    def make_error_tab(self, test, state):
-        msg = '%s (%s)\n%s' % (test.label_en, test.label_zh,
-                               str(state.error_msg))
-        label = make_label(msg, font=self.LABEL_EN_FONT, alignment=(0.0, 0.0))
-        label.set_line_wrap(True)
-        frame = gtk.Frame()
-        frame.add(label)
-        return frame
+  def make_error_tab(self, test, state):
+    msg = '%s (%s)\n%s' % (test.label_en, test.label_zh,
+                str(state.error_msg))
+    label = make_label(msg, font=self.LABEL_EN_FONT, alignment=(0.0, 0.0))
+    label.set_line_wrap(True)
+    frame = gtk.Frame()
+    frame.add(label)
+    return frame
 
-    def make_widget(self):
-        bg_color = gtk.gdk.Color(0x1000, 0x1000, 0x1000)
-        self.notebook = gtk.Notebook()
-        self.notebook.modify_bg(gtk.STATE_NORMAL, bg_color)
+  def make_widget(self):
+    bg_color = gtk.gdk.Color(0x1000, 0x1000, 0x1000)
+    self.notebook = gtk.Notebook()
+    self.notebook.modify_bg(gtk.STATE_NORMAL, bg_color)
 
-        test_list = self.test_list
-        state_map = test_list.get_state_map()
-        tab, _ = make_summary_box([test_list], state_map)
-        tab.set_border_width(self.TAB_BORDER)
-        self.notebook.append_page(tab, make_label('Summary'))
+    test_list = self.test_list
+    state_map = test_list.get_state_map()
+    tab, _ = make_summary_box([test_list], state_map)
+    tab.set_border_width(self.TAB_BORDER)
+    self.notebook.append_page(tab, make_label('Summary'))
 
-        for i, t in izip(
-            count(1),
-            [t for t in test_list.walk()
-             if state_map[t].status == factory.TestState.FAILED
-             and t.is_leaf()]):
-            tab = self.make_error_tab(t, state_map[t])
-            tab.set_border_width(self.TAB_BORDER)
-            self.notebook.append_page(tab, make_label('#%02d' % i))
+    for i, t in izip(
+      count(1),
+      [t for t in test_list.walk()
+       if state_map[t].status == factory.TestState.FAILED
+       and t.is_leaf()]):
+      tab = self.make_error_tab(t, state_map[t])
+      tab.set_border_width(self.TAB_BORDER)
+      self.notebook.append_page(tab, make_label('#%02d' % i))
 
-        prompt = 'Review: Test Status Information'
-        if self.notebook.get_n_pages() > 1:
-            prompt += '\nPress left/right to change tabs'
+    prompt = 'Review: Test Status Information'
+    if self.notebook.get_n_pages() > 1:
+      prompt += '\nPress left/right to change tabs'
 
-        control_label = make_label(prompt, font=self.LABEL_EN_FONT,
-                                   alignment=(0.5, 0.5))
-        vbox = gtk.VBox()
-        vbox.set_spacing(self.TAB_BORDER)
-        vbox.pack_start(control_label, False, False)
-        vbox.pack_start(self.notebook, False, False)
-        vbox.show_all()
-        vbox.grab_focus = self.notebook.grab_focus
-        return vbox
+    control_label = make_label(prompt, font=self.LABEL_EN_FONT,
+                  alignment=(0.5, 0.5))
+    vbox = gtk.VBox()
+    vbox.set_spacing(self.TAB_BORDER)
+    vbox.pack_start(control_label, False, False)
+    vbox.pack_start(self.notebook, False, False)
+    vbox.show_all()
+    vbox.grab_focus = self.notebook.grab_focus
+    return vbox
 
 
 class TestDirectory(gtk.VBox):
-    '''Widget containing a list of tests, colored by test status.
+  '''Widget containing a list of tests, colored by test status.
 
-    This is the widget corresponding to the RHS test panel.
+  This is the widget corresponding to the RHS test panel.
 
-    Attributes:
-      _label_map: Dict of test path to TestLabelBox objects.  Should
-          contain an entry for each test that has been visible at some
-          time.
-      _visible_status: List of (test, status) pairs reflecting the
-          last refresh of the set of visible tests.  This is used to
-          rememeber what tests were active, to allow implementation of
-          visual refresh only when new active tests appear.
-      _shortcut_map: Dict of keyboard shortcut key to test path.
-          Tracks the current set of keyboard shortcut mappings for the
-          visible set of tests.  This will change when the visible
-          test set changes.
+  Attributes:
+   _label_map: Dict of test path to TestLabelBox objects. Should
+     contain an entry for each test that has been visible at some
+     time.
+   _visible_status: List of (test, status) pairs reflecting the
+     last refresh of the set of visible tests. This is used to
+     rememeber what tests were active, to allow implementation of
+     visual refresh only when new active tests appear.
+   _shortcut_map: Dict of keyboard shortcut key to test path.
+     Tracks the current set of keyboard shortcut mappings for the
+     visible set of tests. This will change when the visible
+     test set changes.
+  '''
+
+  def __init__(self, test_list):
+    gtk.VBox.__init__(self)
+    self.set_spacing(0)
+    self._label_map = {}
+    self._visible_status = []
+    self._shortcut_map = {}
+    self._hard_shortcuts = set(
+      test.kbd_shortcut for test in test_list.walk()
+      if test.kbd_shortcut is not None)
+
+  def _get_test_label(self, test):
+    if test.path in self._label_map:
+      return self._label_map[test.path]
+    label_box = TestLabelBox(test)
+    self._label_map[test.path] = label_box
+    return label_box
+
+  def _remove_shortcut(self, path):
+    reverse_map = dict((v, k) for k, v in self._shortcut_map.items())
+    if path not in reverse_map:
+      logging.error('Removal of non-present shortcut for %s' % path)
+      return
+    shortcut = reverse_map[path]
+    del self._shortcut_map[shortcut]
+
+  def _add_shortcut(self, test):
+    shortcut = test.kbd_shortcut
+    if shortcut in self._shortcut_map:
+      logging.error('Shortcut %s already in use by %s; cannot apply to %s'
+             % (shortcut, self._shortcut_map[shortcut], test.path))
+      shortcut = None
+    if shortcut is None:
+      # Find a suitable shortcut. For groups, use numbers. For
+      # regular tests, use alpha (letters).
+      if test.is_group():
+        gen = (x for x in string.digits if x not in self._shortcut_map)
+      else:
+        gen = (x for x in test.label_en.lower() + string.lowercase
+            if x.isalnum() and x not in self._shortcut_map
+            and x not in self._hard_shortcuts)
+      shortcut = next(gen, None)
+    if shortcut is None:
+      logging.error('Unable to find shortcut for %s' % test.path)
+      return
+    self._shortcut_map[shortcut] = test.path
+    return shortcut
+
+  def handle_xevent(self, dummy_src, dummy_cond,
+           xhandle, keycode_map, event_client):
+    for dummy_i in range(0, xhandle.pending_events()):
+      xevent = xhandle.next_event()
+      if xevent.type != X.KeyPress:
+        continue
+      keycode = xevent.detail
+      if keycode not in keycode_map:
+        logging.warning('Ignoring unknown keycode %r' % keycode)
+        continue
+      shortcut = keycode_map[keycode]
+
+      if (xevent.state & GLOBAL_HOT_KEY_MASK == GLOBAL_HOT_KEY_MASK):
+        event_type = GLOBAL_HOT_KEY_EVENTS.get(shortcut)
+        if event_type:
+          event_client.post_event(Event(event_type))
+        else:
+          logging.warning('Unbound global hot key %s', key)
+      else:
+        if shortcut not in self._shortcut_map:
+          logging.warning('Ignoring unbound shortcut %r' % shortcut)
+          continue
+        test_path = self._shortcut_map[shortcut]
+        event_client.post_event(Event(Event.Type.SWITCH_TEST,
+                       path=test_path))
+    return True
+
+  def update(self, new_test_status):
+    '''Refresh the RHS test list to show current status and active groups.
+
+    Refresh the set of visible tests only when new active tests
+    arise. This avoids visual volatility when switching between
+    tests (intervals where no test is active). Also refresh at
+    initial startup.
+
+    Args:
+     new_test_status: A list of (test, status) tuples. The tests
+       order should match how they should be displayed in the
+       directory (rhs panel).
     '''
+    old_active = set(t for t, s in self._visible_status
+             if s == TestState.ACTIVE)
+    new_active = set(t for t, s in new_test_status
+             if s == TestState.ACTIVE)
+    new_visible = set(t for t, s in new_test_status)
+    old_visible = set(t for t, s in self._visible_status)
 
-    def __init__(self, test_list):
-        gtk.VBox.__init__(self)
-        self.set_spacing(0)
-        self._label_map = {}
-        self._visible_status = []
-        self._shortcut_map = {}
-        self._hard_shortcuts = set(
-            test.kbd_shortcut for test in test_list.walk()
-            if test.kbd_shortcut is not None)
+    if old_active and not new_active - old_active:
+      # No new active tests, so do not change the displayed test
+      # set, only update the displayed status for currently
+      # visible tests. Not updating _visible_status allows us
+      # to remember the last set of active tests.
+      for test, _ in self._visible_status:
+        status = test.get_state().status
+        self._label_map[test.path].update(status)
+      return
 
-    def _get_test_label(self, test):
-        if test.path in self._label_map:
-            return self._label_map[test.path]
-        label_box = TestLabelBox(test)
-        self._label_map[test.path] = label_box
-        return label_box
+    self._visible_status = new_test_status
 
-    def _remove_shortcut(self, path):
-        reverse_map = dict((v, k) for k, v in self._shortcut_map.items())
-        if path not in reverse_map:
-            logging.error('Removal of non-present shortcut for %s' % path)
-            return
-        shortcut = reverse_map[path]
-        del self._shortcut_map[shortcut]
+    new_test_map = dict((t.path, t) for t, s in new_test_status)
 
-    def _add_shortcut(self, test):
-        shortcut = test.kbd_shortcut
-        if shortcut in self._shortcut_map:
-            logging.error('Shortcut %s already in use by %s; cannot apply to %s'
-                          % (shortcut, self._shortcut_map[shortcut], test.path))
-            shortcut = None
-        if shortcut is None:
-            # Find a suitable shortcut.  For groups, use numbers.  For
-            # regular tests, use alpha (letters).
-            if test.is_group():
-                gen = (x for x in string.digits if x not in self._shortcut_map)
-            else:
-                gen = (x for x in test.label_en.lower() + string.lowercase
-                       if x.isalnum() and x not in self._shortcut_map
-                       and x not in self._hard_shortcuts)
-            shortcut = next(gen, None)
-        if shortcut is None:
-            logging.error('Unable to find shortcut for %s' % test.path)
-            return
-        self._shortcut_map[shortcut] = test.path
-        return shortcut
+    for test in old_visible - new_visible:
+      label_box = self._label_map[test.path]
+      logging.debug('removing %s test label' % test.path)
+      self.remove(label_box)
+      self._remove_shortcut(test.path)
 
-    def handle_xevent(self, dummy_src, dummy_cond,
-                      xhandle, keycode_map, event_client):
-        for dummy_i in range(0, xhandle.pending_events()):
-            xevent = xhandle.next_event()
-            if xevent.type != X.KeyPress:
-                continue
-            keycode = xevent.detail
-            if keycode not in keycode_map:
-                logging.warning('Ignoring unknown keycode %r' % keycode)
-                continue
-            shortcut = keycode_map[keycode]
+    new_tests = new_visible - old_visible
 
-            if (xevent.state & GLOBAL_HOT_KEY_MASK == GLOBAL_HOT_KEY_MASK):
-                event_type = GLOBAL_HOT_KEY_EVENTS.get(shortcut)
-                if event_type:
-                    event_client.post_event(Event(event_type))
-                else:
-                    logging.warning('Unbound global hot key %s', key)
-            else:
-                if shortcut not in self._shortcut_map:
-                    logging.warning('Ignoring unbound shortcut %r' % shortcut)
-                    continue
-                test_path = self._shortcut_map[shortcut]
-                event_client.post_event(Event(Event.Type.SWITCH_TEST,
-                                              path=test_path))
-        return True
+    for position, (test, status) in enumerate(new_test_status):
+      label_box = self._get_test_label(test)
+      if test in new_tests:
+        shortcut = self._add_shortcut(test)
+        label_box = self._get_test_label(test)
+        label_box.set_shortcut(shortcut)
+        logging.debug('adding %s test label (sortcut %r, pos %d)' %
+               (test.path, shortcut, position))
+        self.pack_start(label_box, False, False)
+      self.reorder_child(label_box, position)
+      label_box.update(status)
 
-    def update(self, new_test_status):
-        '''Refresh the RHS test list to show current status and active groups.
-
-        Refresh the set of visible tests only when new active tests
-        arise.  This avoids visual volatility when switching between
-        tests (intervals where no test is active).  Also refresh at
-        initial startup.
-
-        Args:
-          new_test_status: A list of (test, status) tuples.  The tests
-              order should match how they should be displayed in the
-              directory (rhs panel).
-        '''
-        old_active = set(t for t, s in self._visible_status
-                         if s == TestState.ACTIVE)
-        new_active = set(t for t, s in new_test_status
-                         if s == TestState.ACTIVE)
-        new_visible = set(t for t, s in new_test_status)
-        old_visible = set(t for t, s in self._visible_status)
-
-        if old_active and not new_active - old_active:
-            # No new active tests, so do not change the displayed test
-            # set, only update the displayed status for currently
-            # visible tests.  Not updating _visible_status allows us
-            # to remember the last set of active tests.
-            for test, _ in self._visible_status:
-                status = test.get_state().status
-                self._label_map[test.path].update(status)
-            return
-
-        self._visible_status = new_test_status
-
-        new_test_map = dict((t.path, t) for t, s in new_test_status)
-
-        for test in old_visible - new_visible:
-            label_box = self._label_map[test.path]
-            logging.debug('removing %s test label' % test.path)
-            self.remove(label_box)
-            self._remove_shortcut(test.path)
-
-        new_tests = new_visible - old_visible
-
-        for position, (test, status) in enumerate(new_test_status):
-            label_box = self._get_test_label(test)
-            if test in new_tests:
-                shortcut = self._add_shortcut(test)
-                label_box = self._get_test_label(test)
-                label_box.set_shortcut(shortcut)
-                logging.debug('adding %s test label (sortcut %r, pos %d)' %
-                              (test.path, shortcut, position))
-                self.pack_start(label_box, False, False)
-            self.reorder_child(label_box, position)
-            label_box.update(status)
-
-        self.show_all()
+    self.show_all()
 
 
 
 class UiState(object):
 
-    WIDGET_NONE = 0
-    WIDGET_IDLE = 1
-    WIDGET_SUMMARY = 2
-    WIDGET_REVIEW = 3
+  WIDGET_NONE = 0
+  WIDGET_IDLE = 1
+  WIDGET_SUMMARY = 2
+  WIDGET_REVIEW = 3
 
-    def __init__(self, test_widget_box, test_directory_widget, test_list):
-        self._test_widget_box = test_widget_box
-        self._test_directory_widget = test_directory_widget
-        self._test_list = test_list
-        self._transition_count = 0
-        self._active_test_label_map = None
-        self._active_widget = self.WIDGET_NONE
-        self.update_test_state()
+  def __init__(self, test_widget_box, test_directory_widget, test_list):
+    self._test_widget_box = test_widget_box
+    self._test_directory_widget = test_directory_widget
+    self._test_list = test_list
+    self._transition_count = 0
+    self._active_test_label_map = None
+    self._active_widget = self.WIDGET_NONE
+    self.update_test_state()
 
-    def show_idle_widget(self):
-        self.remove_state_widget()
-        self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
-        self._test_widget_box.set_padding(0, 0, 0, 0)
-        label = make_label(MESSAGE_NO_ACTIVE_TESTS,
-                           font=_OTHER_LABEL_FONT,
-                           alignment=(0.5, 0.5))
-        self._test_widget_box.add(label)
-        self._test_widget_box.show_all()
-        self._active_widget = self.WIDGET_IDLE
+  def show_idle_widget(self):
+    self.remove_state_widget()
+    self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
+    self._test_widget_box.set_padding(0, 0, 0, 0)
+    label = make_label(MESSAGE_NO_ACTIVE_TESTS,
+              font=_OTHER_LABEL_FONT,
+              alignment=(0.5, 0.5))
+    self._test_widget_box.add(label)
+    self._test_widget_box.show_all()
+    self._active_widget = self.WIDGET_IDLE
 
-    def show_summary_widget(self):
-        self.remove_state_widget()
-        state_map = self._test_list.get_state_map()
-        self._test_widget_box.set(0.5, 0.0, 0.0, 0.0)
-        self._test_widget_box.set_padding(40, 0, 0, 0)
-        vbox, self._active_test_label_map = make_summary_box(
-            [t for t in self._test_list.subtests
-             if state_map[t].status == TestState.ACTIVE],
-            state_map)
-        self._test_widget_box.add(vbox)
-        self._test_widget_box.show_all()
-        self._active_widget = self.WIDGET_SUMMARY
+  def show_summary_widget(self):
+    self.remove_state_widget()
+    state_map = self._test_list.get_state_map()
+    self._test_widget_box.set(0.5, 0.0, 0.0, 0.0)
+    self._test_widget_box.set_padding(40, 0, 0, 0)
+    vbox, self._active_test_label_map = make_summary_box(
+      [t for t in self._test_list.subtests
+       if state_map[t].status == TestState.ACTIVE],
+      state_map)
+    self._test_widget_box.add(vbox)
+    self._test_widget_box.show_all()
+    self._active_widget = self.WIDGET_SUMMARY
 
-    def show_review_widget(self):
-        self.remove_state_widget()
-        self._review_request = False
-        self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
-        self._test_widget_box.set_padding(0, 0, 0, 0)
-        widget = ReviewInformation(self._test_list).make_widget()
-        self._test_widget_box.add(widget)
-        self._test_widget_box.show_all()
-        widget.grab_focus()
-        self._active_widget = self.WIDGET_REVIEW
+  def show_review_widget(self):
+    self.remove_state_widget()
+    self._review_request = False
+    self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
+    self._test_widget_box.set_padding(0, 0, 0, 0)
+    widget = ReviewInformation(self._test_list).make_widget()
+    self._test_widget_box.add(widget)
+    self._test_widget_box.show_all()
+    widget.grab_focus()
+    self._active_widget = self.WIDGET_REVIEW
 
-    def remove_state_widget(self):
-        for child in self._test_widget_box.get_children():
-            child.hide()
-            self._test_widget_box.remove(child)
-        self._active_test_label_map = None
-        self._active_widget = self.WIDGET_NONE
+  def remove_state_widget(self):
+    for child in self._test_widget_box.get_children():
+      child.hide()
+      self._test_widget_box.remove(child)
+    self._active_test_label_map = None
+    self._active_widget = self.WIDGET_NONE
 
-    def update_test_state(self):
-        state_map = self._test_list.get_state_map()
-        active_tests = set(
-            t for t in self._test_list.walk()
-            if t.is_leaf() and state_map[t].status == TestState.ACTIVE)
-        active_groups = set(g for t in active_tests
-                            for g in t.get_ancestor_groups())
+  def update_test_state(self):
+    state_map = self._test_list.get_state_map()
+    active_tests = set(
+      t for t in self._test_list.walk()
+      if t.is_leaf() and state_map[t].status == TestState.ACTIVE)
+    active_groups = set(g for t in active_tests
+              for g in t.get_ancestor_groups())
 
-        def filter_visible_test_state(tests):
-            '''List currently visible tests and their status.
+    def filter_visible_test_state(tests):
+      '''List currently visible tests and their status.
 
-            Visible means currently displayed in the RHS panel.
-            Visiblity is implied by being a top level test or having
-            membership in a group with at least one active test.
+      Visible means currently displayed in the RHS panel.
+      Visiblity is implied by being a top level test or having
+      membership in a group with at least one active test.
 
-            Returns:
-              A list of (test, status) tuples for all visible tests,
-              in the order they should be displayed.
-            '''
-            results = []
-            for test in tests:
-                if test.is_group():
-                    results.append((test, TestState.UNTESTED))
-                    if test not in active_groups:
-                        continue
-                    results += filter_visible_test_state(test.subtests)
-                else:
-                    results.append((test, state_map[test].status))
-            return results
+      Returns:
+       A list of (test, status) tuples for all visible tests,
+       in the order they should be displayed.
+      '''
+      results = []
+      for test in tests:
+        if test.is_group():
+          results.append((test, TestState.UNTESTED))
+          if test not in active_groups:
+            continue
+          results += filter_visible_test_state(test.subtests)
+        else:
+          results.append((test, state_map[test].status))
+      return results
 
-        visible_test_state = filter_visible_test_state(self._test_list.subtests)
-        self._test_directory_widget.update(visible_test_state)
+    visible_test_state = filter_visible_test_state(self._test_list.subtests)
+    self._test_directory_widget.update(visible_test_state)
 
-        if not active_tests:
-            # Display the idle or review information screen.
-            def waiting_for_transition():
-                return (self._active_widget not in
-                        [self.WIDGET_REVIEW, self.WIDGET_IDLE])
+    if not active_tests:
+      # Display the idle or review information screen.
+      def waiting_for_transition():
+        return (self._active_widget not in
+            [self.WIDGET_REVIEW, self.WIDGET_IDLE])
 
-            # For smooth transition between tests, idle widget if activated only
-            # after _NO_ACTIVE_TEST_DELAY_MS without state change.
-            def idle_transition_check(cookie):
-                if (waiting_for_transition() and
-                    cookie == self._transition_count):
-                    self._transition_count += 1
-                    self.show_idle_widget()
-                return False
+      # For smooth transition between tests, idle widget if activated only
+      # after _NO_ACTIVE_TEST_DELAY_MS without state change.
+      def idle_transition_check(cookie):
+        if (waiting_for_transition() and
+          cookie == self._transition_count):
+          self._transition_count += 1
+          self.show_idle_widget()
+        return False
 
-            if waiting_for_transition():
-                gobject.timeout_add(_NO_ACTIVE_TEST_DELAY_MS,
-                                    idle_transition_check,
-                                    self._transition_count)
-            return
+      if waiting_for_transition():
+        gobject.timeout_add(_NO_ACTIVE_TEST_DELAY_MS,
+                  idle_transition_check,
+                  self._transition_count)
+      return
 
-        self._transition_count += 1
+    self._transition_count += 1
 
-        if any(t.has_ui for t in active_tests):
-            # Remove the widget (if any) since there is an active test
-            # with a UI.
-            self.remove_state_widget()
-            return
+    if any(t.has_ui for t in active_tests):
+      # Remove the widget (if any) since there is an active test
+      # with a UI.
+      self.remove_state_widget()
+      return
 
-        if (self._active_test_label_map is not None and
-            all(t in self._active_test_label_map for t in active_tests)):
-            # All active tests are already present in the summary, so just
-            # update their states.
-            for test, label in self._active_test_label_map.iteritems():
-                label.modify_fg(
-                    gtk.STATE_NORMAL,
-                    LABEL_COLORS[state_map[test].status])
-            return
+    if (self._active_test_label_map is not None and
+      all(t in self._active_test_label_map for t in active_tests)):
+      # All active tests are already present in the summary, so just
+      # update their states.
+      for test, label in self._active_test_label_map.iteritems():
+        label.modify_fg(
+          gtk.STATE_NORMAL,
+          LABEL_COLORS[state_map[test].status])
+      return
 
-        # No active UI; draw summary of current test states
-        self.show_summary_widget()
+    # No active UI; draw summary of current test states
+    self.show_summary_widget()
 
 
 def grab_shortcut_keys(disp, event_handler, event_client):
-    # We want to receive KeyPress events
-    root = disp.screen().root
-    root.change_attributes(event_mask = X.KeyPressMask)
-    shortcut_set = set(string.lowercase + string.digits)
-    keycode_map = {}
-    for mod, shortcut in ([(X.ControlMask, k) for k in shortcut_set] +
-                          [(GLOBAL_HOT_KEY_MASK, k)
-                           for k in GLOBAL_HOT_KEY_EVENTS] +
-                          [(X.Mod1Mask, 'Tab')]):  # Mod1 = Alt
-        keysym = gtk.gdk.keyval_from_name(shortcut)
-        keycode = disp.keysym_to_keycode(keysym)
-        keycode_map[keycode] = shortcut
-        root.grab_key(keycode, mod, 1, X.GrabModeAsync, X.GrabModeAsync)
-    # This flushes the XGrabKey calls to the server.
-    for dummy_x in range(0, root.display.pending_events()):
-        root.display.next_event()
-    gobject.io_add_watch(root.display, gobject.IO_IN, event_handler,
-                         root.display, keycode_map, event_client)
+  # We want to receive KeyPress events
+  root = disp.screen().root
+  root.change_attributes(event_mask = X.KeyPressMask)
+  shortcut_set = set(string.lowercase + string.digits)
+  keycode_map = {}
+  for mod, shortcut in ([(X.ControlMask, k) for k in shortcut_set] +
+             [(GLOBAL_HOT_KEY_MASK, k)
+              for k in GLOBAL_HOT_KEY_EVENTS] +
+             [(X.Mod1Mask, 'Tab')]): # Mod1 = Alt
+    keysym = gtk.gdk.keyval_from_name(shortcut)
+    keycode = disp.keysym_to_keycode(keysym)
+    keycode_map[keycode] = shortcut
+    root.grab_key(keycode, mod, 1, X.GrabModeAsync, X.GrabModeAsync)
+  # This flushes the XGrabKey calls to the server.
+  for dummy_x in range(0, root.display.pending_events()):
+    root.display.next_event()
+  gobject.io_add_watch(root.display, gobject.IO_IN, event_handler,
+             root.display, keycode_map, event_client)
 
 
 def start_reposition_thread(title_regexp):
-    '''Starts a thread to reposition a client window once it appears.
+  '''Starts a thread to reposition a client window once it appears.
 
-    This is useful to avoid blocking the console.
+  This is useful to avoid blocking the console.
 
-    Args:
-      title_regexp: A regexp for the window's title (used to find the
-        window to reposition).
-    '''
-    test_widget_position = (
-        factory.get_shared_data('test_widget_position'))
-    if not test_widget_position:
+  Args:
+   title_regexp: A regexp for the window's title (used to find the
+    window to reposition).
+  '''
+  test_widget_position = (
+    factory.get_shared_data('test_widget_position'))
+  if not test_widget_position:
+    return
+
+  def reposition():
+    display = Display()
+    root = display.screen().root
+    for i in xrange(50):
+      wins = [win for win in root.query_tree().children
+          if re.match(title_regexp, win.get_wm_name())]
+      if wins:
+        wins[0].configure(x=test_widget_position[0],
+                 y=test_widget_position[1])
+        display.sync()
         return
-
-    def reposition():
-        display = Display()
-        root = display.screen().root
-        for i in xrange(50):
-            wins = [win for win in root.query_tree().children
-                    if re.match(title_regexp, win.get_wm_name())]
-            if wins:
-                wins[0].configure(x=test_widget_position[0],
-                                  y=test_widget_position[1])
-                display.sync()
-                return
-            # Wait 100 ms and try again.
-            time.sleep(.1)
-    thread = threading.Thread(target=reposition)
-    thread.daemon = True
-    thread.start()
+      # Wait 100 ms and try again.
+      time.sleep(.1)
+  thread = threading.Thread(target=reposition)
+  thread.daemon = True
+  thread.start()
 
 
 def main(test_list_path):
-    '''Starts the main UI.
+  '''Starts the main UI.
 
-    This is launched by the autotest/cros/factory/client.
-    When operators press keyboard shortcuts, the shortcut
-    value is sent as an event to the control program.'''
+  This is launched by the autotest/cros/factory/client.
+  When operators press keyboard shortcuts, the shortcut
+  value is sent as an event to the control program.'''
 
-    test_list = None
-    ui_state = None
-    event_client = None
+  test_list = None
+  ui_state = None
+  event_client = None
 
-    def handle_key_release_event(_, event):
-        logging.info('base ui key event (%s)', event.keyval)
-        return True
+  def handle_key_release_event(_, event):
+    logging.info('base ui key event (%s)', event.keyval)
+    return True
 
-    def handle_event(event):
-        if event.type == Event.Type.STATE_CHANGE:
-            ui_state.update_test_state()
-        elif event.type == Event.Type.REVIEW:
-            logging.info("Operator activates review information screen")
-            ui_state.show_review_widget()
+  def handle_event(event):
+    if event.type == Event.Type.STATE_CHANGE:
+      ui_state.update_test_state()
+    elif event.type == Event.Type.REVIEW:
+      logging.info("Operator activates review information screen")
+      ui_state.show_review_widget()
 
-    test_list = factory.read_test_list(test_list_path)
+  test_list = factory.read_test_list(test_list_path)
 
-    window = gtk.Window(gtk.WINDOW_TOPLEVEL)
-    window.connect('destroy', lambda _: gtk.main_quit())
-    window.modify_bg(gtk.STATE_NORMAL, BLACK)
+  window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+  window.connect('destroy', lambda _: gtk.main_quit())
+  window.modify_bg(gtk.STATE_NORMAL, BLACK)
 
-    disp = Display()
+  disp = Display()
 
-    event_client = EventClient(
-        callback=handle_event,
-        event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
+  event_client = EventClient(
+    callback=handle_event,
+    event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
 
-    screen = window.get_screen()
-    if (screen is None):
-        logging.info('ERROR: communication with the X server is not working, ' +
-                    'could not find a working screen.  UI exiting.')
-        sys.exit(1)
+  screen = window.get_screen()
+  if (screen is None):
+    logging.info('ERROR: communication with the X server is not working, ' +
+          'could not find a working screen. UI exiting.')
+    sys.exit(1)
 
-    screen_size_str = os.environ.get('CROS_SCREEN_SIZE')
-    if screen_size_str:
-        match = re.match(r'^(\d+)x(\d+)$', screen_size_str)
-        assert match, 'CROS_SCREEN_SIZE should be {width}x{height}'
-        screen_size = (int(match.group(1)), int(match.group(2)))
-    else:
-        screen_size = (screen.get_width(), screen.get_height())
-    window.set_size_request(*screen_size)
+  screen_size_str = os.environ.get('CROS_SCREEN_SIZE')
+  if screen_size_str:
+    match = re.match(r'^(\d+)x(\d+)$', screen_size_str)
+    assert match, 'CROS_SCREEN_SIZE should be {width}x{height}'
+    screen_size = (int(match.group(1)), int(match.group(2)))
+  else:
+    screen_size = (screen.get_width(), screen.get_height())
+  window.set_size_request(*screen_size)
 
-    test_directory = TestDirectory(test_list)
+  test_directory = TestDirectory(test_list)
 
-    rhs_box = gtk.EventBox()
-    rhs_box.modify_bg(gtk.STATE_NORMAL, _LABEL_TROUGH_COLOR)
-    rhs_box.add(test_directory)
+  rhs_box = gtk.EventBox()
+  rhs_box.modify_bg(gtk.STATE_NORMAL, _LABEL_TROUGH_COLOR)
+  rhs_box.add(test_directory)
 
-    console_box = gtk.EventBox()
-    console_box.set_size_request(*convert_pixels((-1, 180)))
-    console_box.modify_bg(gtk.STATE_NORMAL, BLACK)
+  console_box = gtk.EventBox()
+  console_box.set_size_request(*convert_pixels((-1, 180)))
+  console_box.modify_bg(gtk.STATE_NORMAL, BLACK)
 
-    test_widget_box = gtk.Alignment()
-    test_widget_box.set_size_request(-1, -1)
+  test_widget_box = gtk.Alignment()
+  test_widget_box.set_size_request(-1, -1)
 
-    lhs_box = gtk.VBox()
-    lhs_box.pack_end(console_box, False, False)
-    lhs_box.pack_start(test_widget_box)
-    lhs_box.pack_start(make_hsep(3), False, False)
+  lhs_box = gtk.VBox()
+  lhs_box.pack_end(console_box, False, False)
+  lhs_box.pack_start(test_widget_box)
+  lhs_box.pack_start(make_hsep(3), False, False)
 
-    base_box = gtk.HBox()
-    base_box.pack_end(rhs_box, False, False)
-    base_box.pack_end(make_vsep(3), False, False)
-    base_box.pack_start(lhs_box)
+  base_box = gtk.HBox()
+  base_box.pack_end(rhs_box, False, False)
+  base_box.pack_end(make_vsep(3), False, False)
+  base_box.pack_start(lhs_box)
 
-    window.connect('key-release-event', handle_key_release_event)
-    window.add_events(gtk.gdk.KEY_RELEASE_MASK)
+  window.connect('key-release-event', handle_key_release_event)
+  window.add_events(gtk.gdk.KEY_RELEASE_MASK)
 
-    ui_state = UiState(test_widget_box, test_directory, test_list)
+  ui_state = UiState(test_widget_box, test_directory, test_list)
 
-    window.add(base_box)
-    window.show_all()
+  window.add(base_box)
+  window.show_all()
 
-    grab_shortcut_keys(disp, test_directory.handle_xevent, event_client)
+  grab_shortcut_keys(disp, test_directory.handle_xevent, event_client)
 
-    hide_cursor(window.window)
+  hide_cursor(window.window)
 
-    test_widget_allocation = test_widget_box.get_allocation()
-    test_widget_size = (test_widget_allocation.width,
-                        test_widget_allocation.height)
-    factory.set_shared_data('test_widget_size', test_widget_size)
+  test_widget_allocation = test_widget_box.get_allocation()
+  test_widget_size = (test_widget_allocation.width,
+            test_widget_allocation.height)
+  factory.set_shared_data('test_widget_size', test_widget_size)
 
-    if not factory.in_chroot():
-        dummy_console = Console(console_box.get_allocation())
+  if not factory.in_chroot():
+    dummy_console = Console(console_box.get_allocation())
 
-    event_client.post_event(Event(Event.Type.UI_READY))
+  event_client.post_event(Event(Event.Type.UI_READY))
 
-    logging.info('cros/factory/ui setup done, starting gtk.main()...')
-    gtk.main()
-    logging.info('cros/factory/ui gtk.main() finished, exiting.')
+  logging.info('cros/factory/ui setup done, starting gtk.main()...')
+  gtk.main()
+  logging.info('cros/factory/ui gtk.main() finished, exiting.')
 
 
 if __name__ == '__main__':
-    parser = OptionParser(usage='usage: %prog [options] TEST-LIST-PATH')
-    parser.add_option('-v', '--verbose', dest='verbose',
-                      action='store_true',
-                      help='Enable debug logging')
-    (options, args) = parser.parse_args()
+  parser = OptionParser(usage='usage: %prog [options] TEST-LIST-PATH')
+  parser.add_option('-v', '--verbose', dest='verbose',
+           action='store_true',
+           help='Enable debug logging')
+  (options, args) = parser.parse_args()
 
-    if len(args) != 1:
-        parser.error('Incorrect number of arguments')
+  if len(args) != 1:
+    parser.error('Incorrect number of arguments')
 
-    factory.init_logging('ui', verbose=options.verbose)
-    main(sys.argv[1])
+  factory.init_logging('ui', verbose=options.verbose)
+  main(sys.argv[1])
diff --git a/py/test/unicode_to_string.py b/py/test/unicode_to_string.py
index 0f5ab46..b531e07 100644
--- a/py/test/unicode_to_string.py
+++ b/py/test/unicode_to_string.py
@@ -10,38 +10,38 @@
 
 
 def UnicodeToString(obj):
-    '''Converts any Unicode strings in obj to UTF-8 strings.
+  '''Converts any Unicode strings in obj to UTF-8 strings.
 
-    Recurses into lists, dicts, and tuples in obj.
-    '''
-    if isinstance(obj, list):
-        return [UnicodeToString(x) for x in obj]
-    elif isinstance(obj, dict):
-        return dict((UnicodeToString(k), UnicodeToString(v))
-                    for k, v in obj.iteritems())
-    elif isinstance(obj, unicode):
-        return obj.encode('utf-8')
-    elif isinstance(obj, tuple):
-        return tuple(UnicodeToString(x) for x in obj)
-    elif isinstance(obj, set):
-        return set(UnicodeToString(x) for x in obj)
-    else:
-        return obj
+  Recurses into lists, dicts, and tuples in obj.
+  '''
+  if isinstance(obj, list):
+    return [UnicodeToString(x) for x in obj]
+  elif isinstance(obj, dict):
+    return dict((UnicodeToString(k), UnicodeToString(v))
+          for k, v in obj.iteritems())
+  elif isinstance(obj, unicode):
+    return obj.encode('utf-8')
+  elif isinstance(obj, tuple):
+    return tuple(UnicodeToString(x) for x in obj)
+  elif isinstance(obj, set):
+    return set(UnicodeToString(x) for x in obj)
+  else:
+    return obj
 
 
 def UnicodeToStringArgs(function):
-    '''A function decorator that converts function's arguments from
-    Unicode to strings using UnicodeToString.
-    '''
-    return (lambda *args, **kwargs:
-                function(*UnicodeToString(args),
-                          **UnicodeToString(kwargs)))
+  '''A function decorator that converts function's arguments from
+  Unicode to strings using UnicodeToString.
+  '''
+  return (lambda *args, **kwargs:
+        function(*UnicodeToString(args),
+             **UnicodeToString(kwargs)))
 
 
 def UnicodeToStringClass(cls):
-    '''A class decorator that converts all arguments of all
-    methods in class from Unicode to strings using UnicodeToStringArgs.'''
-    for k, v in cls.__dict__.items():
-        if type(v) == types.FunctionType:
-            setattr(cls, k, UnicodeToStringArgs(v))
-    return cls
+  '''A class decorator that converts all arguments of all
+  methods in class from Unicode to strings using UnicodeToStringArgs.'''
+  for k, v in cls.__dict__.items():
+    if type(v) == types.FunctionType:
+      setattr(cls, k, UnicodeToStringArgs(v))
+  return cls
diff --git a/py/test/unicode_to_string_unittest.py b/py/test/unicode_to_string_unittest.py
index 169a4493..8930b18 100755
--- a/py/test/unicode_to_string_unittest.py
+++ b/py/test/unicode_to_string_unittest.py
@@ -5,88 +5,88 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import factory_common
+import factory_common # pylint: disable=W0611
 
 import unittest
 from cros.factory.test.unicode_to_string \
-    import UnicodeToString, UnicodeToStringArgs, UnicodeToStringClass
+  import UnicodeToString, UnicodeToStringArgs, UnicodeToStringClass
 
 # My favorite character: 囧
 JIONG_UTF8 = '\xe5\x9b\xa7'
 
 class UnicodeToStringTest(unittest.TestCase):
-    def isSame(self, a, b):
-        '''Returns True if a and b are equal and the same type.
+  def isSame(self, a, b):
+    '''Returns True if a and b are equal and the same type.
 
-        This is necessary because 'abc' == u'abc' but we want to distinguish
-        them.
-        '''
-        if a != b:
-            return False
-        elif type(a) != type(b):
-            return False
-        elif type(a) in [list, tuple]:
-            for x, y in zip(a, b):
-                if not self.isSame(x, y):
-                    return False
-        elif type(a) == set:
-            return self.isSame(sorted(list(a)), sorted(list(b)))
-        elif type(a) == dict:
-            for k in a:
-                if not self.isSame(a[k], b[k]):
-                    return False
-        return True
+    This is necessary because 'abc' == u'abc' but we want to distinguish
+    them.
+    '''
+    if a != b:
+      return False
+    elif type(a) != type(b):
+      return False
+    elif type(a) in [list, tuple]:
+      for x, y in zip(a, b):
+        if not self.isSame(x, y):
+          return False
+    elif type(a) == set:
+      return self.isSame(sorted(list(a)), sorted(list(b)))
+    elif type(a) == dict:
+      for k in a:
+        if not self.isSame(a[k], b[k]):
+          return False
+    return True
 
-    def assertSame(self, a, b):
-        self.assertTrue(self.isSame(a, b), 'isSame(%r,%r)' % (a, b))
+  def assertSame(self, a, b):
+    self.assertTrue(self.isSame(a, b), 'isSame(%r,%r)' % (a, b))
 
-    def testAssertSame(self):
-        '''Makes sense that assertSame works properly.'''
-        self.assertSame('abc', 'abc')
-        self.assertRaises(AssertionError,
-                          lambda: self.assertSame('abc', u'abc'))
-        self.assertSame(['a'], ['a'])
-        self.assertRaises(AssertionError,
-                          lambda: self.assertSame(['a'], [u'a']))
-        self.assertSame(('a'), ('a'))
-        self.assertRaises(AssertionError,
-                          lambda: self.assertSame(('a'), (u'a')))
-        self.assertSame(set(['a']), set(['a']))
-        self.assertRaises(
-            AssertionError,
-            lambda: self.assertSame(set(['a']),
-                                    set([u'a'])))
-        self.assertSame({1: 'a'}, {1: 'a'})
-        self.assertRaises(AssertionError,
-                          lambda: self.assertSame({1: 'a'}, {1: u'a'}))
+  def testAssertSame(self):
+    '''Makes sense that assertSame works properly.'''
+    self.assertSame('abc', 'abc')
+    self.assertRaises(AssertionError,
+             lambda: self.assertSame('abc', u'abc'))
+    self.assertSame(['a'], ['a'])
+    self.assertRaises(AssertionError,
+             lambda: self.assertSame(['a'], [u'a']))
+    self.assertSame(('a'), ('a'))
+    self.assertRaises(AssertionError,
+             lambda: self.assertSame(('a'), (u'a')))
+    self.assertSame(set(['a']), set(['a']))
+    self.assertRaises(
+      AssertionError,
+      lambda: self.assertSame(set(['a']),
+                  set([u'a'])))
+    self.assertSame({1: 'a'}, {1: 'a'})
+    self.assertRaises(AssertionError,
+             lambda: self.assertSame({1: 'a'}, {1: u'a'}))
 
-    def testUnicodeToString(self):
-        self.assertSame(1, UnicodeToString(1))
-        self.assertSame('abc', UnicodeToString(u'abc'))
-        self.assertSame(JIONG_UTF8, UnicodeToString(u'囧'))
+  def testUnicodeToString(self):
+    self.assertSame(1, UnicodeToString(1))
+    self.assertSame('abc', UnicodeToString(u'abc'))
+    self.assertSame(JIONG_UTF8, UnicodeToString(u'囧'))
 
-    def testUnicodeToStringArgs(self):
-        @UnicodeToStringArgs
-        def func(*args, **kwargs):
-            return ('func', args, kwargs)
+  def testUnicodeToStringArgs(self):
+    @UnicodeToStringArgs
+    def func(*args, **kwargs):
+      return ('func', args, kwargs)
 
-        self.assertSame(('func', ('a',), {'b': 'c'}),
-                        func(u'a', b=u'c'))
+    self.assertSame(('func', ('a',), {'b': 'c'}),
+            func(u'a', b=u'c'))
 
-    def testUnicodeToStringClass(self):
-        @UnicodeToStringClass
-        class MyClass(object):
-            def f1(self, *args, **kwargs):
-                return ('f1', args, kwargs)
-            def f2(self, *args, **kwargs):
-                return ('f2', args, kwargs)
+  def testUnicodeToStringClass(self):
+    @UnicodeToStringClass
+    class MyClass(object):
+      def f1(self, *args, **kwargs):
+        return ('f1', args, kwargs)
+      def f2(self, *args, **kwargs):
+        return ('f2', args, kwargs)
 
-        obj = MyClass()
-        self.assertSame(('f1', ('a',), {'b': 'c', 'd': set(['e'])}),
-                        obj.f1(u'a', b=u'c', d=set([u'e'])))
-        self.assertSame(('f2', ('a',), {'b': 'c', 'd': set(['e'])}),
-                        obj.f2(u'a', b=u'c', d=set([u'e'])))
+    obj = MyClass()
+    self.assertSame(('f1', ('a',), {'b': 'c', 'd': set(['e'])}),
+            obj.f1(u'a', b=u'c', d=set([u'e'])))
+    self.assertSame(('f2', ('a',), {'b': 'c', 'd': set(['e'])}),
+            obj.f2(u'a', b=u'c', d=set([u'e'])))
 
 
 if __name__ == "__main__":
-    unittest.main()
+  unittest.main()
diff --git a/py/test/utils.py b/py/test/utils.py
index acbfc30..7721b77 100644
--- a/py/test/utils.py
+++ b/py/test/utils.py
@@ -18,178 +18,178 @@
 
 
 def TimeString(unix_time=None):
-    """Returns a time (using UTC) as a string.
+  """Returns a time (using UTC) as a string.
 
-    The format is like ISO8601 but with milliseconds:
+  The format is like ISO8601 but with milliseconds:
 
-      2012-05-22T14:15:08.123Z
-    """
+   2012-05-22T14:15:08.123Z
+  """
 
-    t = unix_time or time.time()
-    return "%s.%03dZ" % (time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(t)),
-                         int((t - int(t)) * 1000))
+  t = unix_time or time.time()
+  return "%s.%03dZ" % (time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(t)),
+             int((t - int(t)) * 1000))
 
 
 def in_chroot():
-    '''Returns True if currently in the chroot.'''
-    return 'CROS_WORKON_SRCROOT' in os.environ
+  '''Returns True if currently in the chroot.'''
+  return 'CROS_WORKON_SRCROOT' in os.environ
 
 
 def in_qemu():
-    '''Returns True if running within QEMU.'''
-    return 'QEMU' in open('/proc/cpuinfo').read()
+  '''Returns True if running within QEMU.'''
+  return 'QEMU' in open('/proc/cpuinfo').read()
 
 
 def is_process_alive(pid):
-    '''
-    Returns true if the named process is alive and not a zombie.
-    '''
-    try:
-        with open("/proc/%d/stat" % pid) as f:
-            return f.readline().split()[2] != 'Z'
-    except IOError:
-        return False
-
-
-def kill_process_tree(process, caption):
-    '''
-    Kills a process and all its subprocesses.
-
-    @param process: The process to kill (opened with the subprocess module).
-    @param caption: A caption describing the process.
-    '''
-    # os.kill does not kill child processes. os.killpg kills all processes
-    # sharing same group (and is usually used for killing process tree). But in
-    # our case, to preserve PGID for autotest and upstart service, we need to
-    # iterate through each level until leaf of the tree.
-
-    def get_all_pids(root):
-        ps_output = subprocess.Popen(['ps','--no-headers','-eo','pid,ppid'],
-                                     stdout=subprocess.PIPE)
-        children = {}
-        for line in ps_output.stdout:
-            match = re.findall('\d+', line)
-            children.setdefault(int(match[1]), []).append(int(match[0]))
-        pids = []
-        def add_children(pid):
-            pids.append(pid)
-            map(add_children, children.get(pid, []))
-        add_children(root)
-        # Reverse the list to first kill children then parents.
-        # Note reversed(pids) will return an iterator instead of real list, so
-        # we must explicitly call pids.reverse() here.
-        pids.reverse()
-        return pids
-
-    pids = get_all_pids(process.pid)
-    for sig in [signal.SIGTERM, signal.SIGKILL]:
-        logging.info('Stopping %s (pid=%s)...', caption, sorted(pids))
-
-        for i in range(25):  # Try 25 times (200 ms between tries)
-            for pid in pids:
-                try:
-                    logging.info("Sending signal %s to %d", sig, pid)
-                    os.kill(pid, sig)
-                except OSError:
-                    pass
-            pids = filter(is_process_alive, pids)
-            if not pids:
-                return
-            time.sleep(0.2)  # Sleep 200 ms and try again
-
-    logging.warn('Failed to stop %s process. Ignoring.', caption)
-
-
-def are_shift_keys_depressed():
-    '''Returns True if both shift keys are depressed.'''
-    # From #include <linux/input.h>
-    KEY_LEFTSHIFT = 42
-    KEY_RIGHTSHIFT = 54
-
-    for kbd in glob.glob("/dev/input/by-path/*kbd"):
-        try:
-            f = os.open(kbd, os.O_RDONLY)
-        except OSError as e:
-            if in_chroot():
-                # That's OK; we're just not root
-                continue
-            else:
-                raise
-        buf = array.array('b', [0] * 96)
-
-        # EVIOCGKEY (from #include <linux/input.h>)
-        fcntl.ioctl(f, 0x80604518, buf)
-
-        def is_pressed(key):
-            return (buf[key / 8] & (1 << (key % 8))) != 0
-
-        if is_pressed(KEY_LEFTSHIFT) and is_pressed(KEY_RIGHTSHIFT):
-            return True
-
+  '''
+  Returns true if the named process is alive and not a zombie.
+  '''
+  try:
+    with open("/proc/%d/stat" % pid) as f:
+      return f.readline().split()[2] != 'Z'
+  except IOError:
     return False
 
 
+def kill_process_tree(process, caption):
+  '''
+  Kills a process and all its subprocesses.
+
+  @param process: The process to kill (opened with the subprocess module).
+  @param caption: A caption describing the process.
+  '''
+  # os.kill does not kill child processes. os.killpg kills all processes
+  # sharing same group (and is usually used for killing process tree). But in
+  # our case, to preserve PGID for autotest and upstart service, we need to
+  # iterate through each level until leaf of the tree.
+
+  def get_all_pids(root):
+    ps_output = subprocess.Popen(['ps','--no-headers','-eo','pid,ppid'],
+                   stdout=subprocess.PIPE)
+    children = {}
+    for line in ps_output.stdout:
+      match = re.findall('\d+', line)
+      children.setdefault(int(match[1]), []).append(int(match[0]))
+    pids = []
+    def add_children(pid):
+      pids.append(pid)
+      map(add_children, children.get(pid, []))
+    add_children(root)
+    # Reverse the list to first kill children then parents.
+    # Note reversed(pids) will return an iterator instead of real list, so
+    # we must explicitly call pids.reverse() here.
+    pids.reverse()
+    return pids
+
+  pids = get_all_pids(process.pid)
+  for sig in [signal.SIGTERM, signal.SIGKILL]:
+    logging.info('Stopping %s (pid=%s)...', caption, sorted(pids))
+
+    for i in range(25): # Try 25 times (200 ms between tries)
+      for pid in pids:
+        try:
+          logging.info("Sending signal %s to %d", sig, pid)
+          os.kill(pid, sig)
+        except OSError:
+          pass
+      pids = filter(is_process_alive, pids)
+      if not pids:
+        return
+      time.sleep(0.2) # Sleep 200 ms and try again
+
+  logging.warn('Failed to stop %s process. Ignoring.', caption)
+
+
+def are_shift_keys_depressed():
+  '''Returns True if both shift keys are depressed.'''
+  # From #include <linux/input.h>
+  KEY_LEFTSHIFT = 42
+  KEY_RIGHTSHIFT = 54
+
+  for kbd in glob.glob("/dev/input/by-path/*kbd"):
+    try:
+      f = os.open(kbd, os.O_RDONLY)
+    except OSError as e:
+      if in_chroot():
+        # That's OK; we're just not root
+        continue
+      else:
+        raise
+    buf = array.array('b', [0] * 96)
+
+    # EVIOCGKEY (from #include <linux/input.h>)
+    fcntl.ioctl(f, 0x80604518, buf)
+
+    def is_pressed(key):
+      return (buf[key / 8] & (1 << (key % 8))) != 0
+
+    if is_pressed(KEY_LEFTSHIFT) and is_pressed(KEY_RIGHTSHIFT):
+      return True
+
+  return False
+
+
 def var_log_messages_before_reboot(lines=100,
-                                   max_length=1024*1024,
-                                   path='/var/log/messages'):
-    '''Returns the last few lines in /var/log/messages
-    before the current boot.
+                  max_length=1024*1024,
+                  path='/var/log/messages'):
+  '''Returns the last few lines in /var/log/messages
+  before the current boot.
 
-    Returns:
-        An array of lines.  Empty if the marker indicating kernel boot
-        could not be found.
+  Returns:
+    An array of lines. Empty if the marker indicating kernel boot
+    could not be found.
 
-    Args:
-        lines: number of lines to return.
-        max_length: maximum amount of data at end of file to read.
-        path: path to /var/log/messages.
-    '''
-    offset = max(0, os.path.getsize(path) - max_length)
-    with open(path) as f:
-        f.seek(offset)
-        data = f.read()
+  Args:
+    lines: number of lines to return.
+    max_length: maximum amount of data at end of file to read.
+    path: path to /var/log/messages.
+  '''
+  offset = max(0, os.path.getsize(path) - max_length)
+  with open(path) as f:
+    f.seek(offset)
+    data = f.read()
 
-    # Find the last element matching the RE signaling kernel start.
-    matches = list(re.finditer(r'kernel:\s+\[\s+0\.\d+\] Linux version', data))
-    if not matches:
-        return []
+  # Find the last element matching the RE signaling kernel start.
+  matches = list(re.finditer(r'kernel:\s+\[\s+0\.\d+\] Linux version', data))
+  if not matches:
+    return []
 
-    match = matches[-1]
-    tail_lines = data[:match.start()].split('\n')
-    tail_lines.pop()  # Remove incomplete line at end
+  match = matches[-1]
+  tail_lines = data[:match.start()].split('\n')
+  tail_lines.pop() # Remove incomplete line at end
 
-    # Skip some common lines that may have been written before the Linux
-    # version.
-    while tail_lines and any(
-        re.search(x, tail_lines[-1])
-        for x in [r'0\.000000\]',
-                  r'rsyslogd.+\(re\)start',
-                  r'/proc/kmsg started']):
-        tail_lines.pop()
+  # Skip some common lines that may have been written before the Linux
+  # version.
+  while tail_lines and any(
+    re.search(x, tail_lines[-1])
+    for x in [r'0\.000000\]',
+         r'rsyslogd.+\(re\)start',
+         r'/proc/kmsg started']):
+    tail_lines.pop()
 
-    # Done!  Return the last few lines.
-    return tail_lines[-lines:]
+  # Done! Return the last few lines.
+  return tail_lines[-lines:]
 
 
 def DrainQueue(queue):
-    '''
-    Returns as many elements as can be obtained from a queue
-    without blocking.
+  '''
+  Returns as many elements as can be obtained from a queue
+  without blocking.
 
-    (This may be no elements at all.)
-    '''
-    ret = []
-    while True:
-        try:
-            ret.append(queue.get_nowait())
-        except Queue.Empty:
-            break
-    return ret
+  (This may be no elements at all.)
+  '''
+  ret = []
+  while True:
+    try:
+      ret.append(queue.get_nowait())
+    except Queue.Empty:
+      break
+  return ret
 
 
 class Enum(frozenset):
-    '''An enumeration type.'''
-    def __getattr__(self, name):
-        if name in self:
-            return name
-        raise AttributeError
+  '''An enumeration type.'''
+  def __getattr__(self, name):
+    if name in self:
+      return name
+    raise AttributeError
diff --git a/py/test/utils_unittest.py b/py/test/utils_unittest.py
index 30152c2..b4c49d6 100755
--- a/py/test/utils_unittest.py
+++ b/py/test/utils_unittest.py
@@ -8,14 +8,14 @@
 import tempfile
 import unittest
 
-import factory_common
+import factory_common # pylint: disable=W0611
 from cros.factory.test import utils
 
 
 EARLIER_VAR_LOG_MESSAGES = '''19:26:17 kernel: That's all, folks.
-19:26:56 kernel: [    0.000000] Initializing cgroup subsys cpuset
-19:26:56 kernel: [    0.000000] Initializing cgroup subsys cpu
-19:26:56 kernel: [    0.000000] Linux version blahblahblah
+19:26:56 kernel: [  0.000000] Initializing cgroup subsys cpuset
+19:26:56 kernel: [  0.000000] Initializing cgroup subsys cpu
+19:26:56 kernel: [  0.000000] Linux version blahblahblah
 '''
 
 VAR_LOG_MESSAGES = '''19:00:00 kernel: 7 p.m. and all's well.
@@ -23,37 +23,37 @@
 19:27:17 kernel: Kernel logging (proc) stopped.
 19:27:56 kernel: imklog 4.6.2, log source = /proc/kmsg started.
 19:27:56 rsyslogd: [origin software="rsyslogd" blahblahblah] (re)start
-19:27:56 kernel: [    0.000000] Initializing cgroup subsys cpuset
-19:27:56 kernel: [    0.000000] Initializing cgroup subsys cpu
-19:27:56 kernel: [    0.000000] Linux version blahblahblah
-19:27:56 kernel: [    0.000000] Command line: blahblahblah
+19:27:56 kernel: [  0.000000] Initializing cgroup subsys cpuset
+19:27:56 kernel: [  0.000000] Initializing cgroup subsys cpu
+19:27:56 kernel: [  0.000000] Linux version blahblahblah
+19:27:56 kernel: [  0.000000] Command line: blahblahblah
 '''
 
 class VarLogMessagesTest(unittest.TestCase):
-    def _GetMessages(self, data, lines):
-        with tempfile.NamedTemporaryFile() as f:
-            path = f.name
-            f.write(data)
-            f.flush()
+  def _GetMessages(self, data, lines):
+    with tempfile.NamedTemporaryFile() as f:
+      path = f.name
+      f.write(data)
+      f.flush()
 
-            return utils.var_log_messages_before_reboot(path=path, lines=lines)
+      return utils.var_log_messages_before_reboot(path=path, lines=lines)
 
-    def runTest(self):
-        self.assertEquals([
-                "19:27:17 kernel: That's all, folks.",
-                "19:27:17 kernel: Kernel logging (proc) stopped.",
-                ], self._GetMessages(VAR_LOG_MESSAGES, 2))
-        self.assertEquals([
-                "19:27:17 kernel: Kernel logging (proc) stopped.",
-                ], self._GetMessages(VAR_LOG_MESSAGES, 1))
-        self.assertEquals([
-                "19:00:00 kernel: 7 p.m. and all's well.",
-                "19:27:17 kernel: That's all, folks.",
-                "19:27:17 kernel: Kernel logging (proc) stopped.",
-                ], self._GetMessages(VAR_LOG_MESSAGES, 100))
-        self.assertEquals([
-                "19:26:17 kernel: That's all, folks.",
-                ], self._GetMessages(EARLIER_VAR_LOG_MESSAGES, 1))
+  def runTest(self):
+    self.assertEquals([
+        "19:27:17 kernel: That's all, folks.",
+        "19:27:17 kernel: Kernel logging (proc) stopped.",
+        ], self._GetMessages(VAR_LOG_MESSAGES, 2))
+    self.assertEquals([
+        "19:27:17 kernel: Kernel logging (proc) stopped.",
+        ], self._GetMessages(VAR_LOG_MESSAGES, 1))
+    self.assertEquals([
+        "19:00:00 kernel: 7 p.m. and all's well.",
+        "19:27:17 kernel: That's all, folks.",
+        "19:27:17 kernel: Kernel logging (proc) stopped.",
+        ], self._GetMessages(VAR_LOG_MESSAGES, 100))
+    self.assertEquals([
+        "19:26:17 kernel: That's all, folks.",
+        ], self._GetMessages(EARLIER_VAR_LOG_MESSAGES, 1))
 
 if __name__ == "__main__":
-    unittest.main()
+  unittest.main()
diff --git a/py/utils/net_utils.py b/py/utils/net_utils.py
index ec49398..79e3509 100644
--- a/py/utils/net_utils.py
+++ b/py/utils/net_utils.py
@@ -12,30 +12,30 @@
 
 
 class TimeoutHTTPConnection(httplib.HTTPConnection):
-    def connect(self):
-        httplib.HTTPConnection.connect(self)
-        self.sock.settimeout(self.timeout)
+  def connect(self):
+    httplib.HTTPConnection.connect(self)
+    self.sock.settimeout(self.timeout)
 
 class TimeoutHTTP(httplib.HTTP):
-    _connection_class = TimeoutHTTPConnection
-    def set_timeout(self, timeout):
-        self._conn.timeout = timeout
+  _connection_class = TimeoutHTTPConnection
+  def set_timeout(self, timeout):
+    self._conn.timeout = timeout
 
 class TimeoutXMLRPCTransport(xmlrpclib.Transport):
-    '''Transport subclass supporting timeout.'''
-    def __init__(self, timeout=DEFAULT_TIMEOUT, *args, **kwargs):
-        xmlrpclib.Transport.__init__(self, *args, **kwargs)
-        self.timeout = timeout
+  '''Transport subclass supporting timeout.'''
+  def __init__(self, timeout=DEFAULT_TIMEOUT, *args, **kwargs):
+    xmlrpclib.Transport.__init__(self, *args, **kwargs)
+    self.timeout = timeout
 
-    def make_connection(self, host):
-        conn = TimeoutHTTP(host)
-        conn.set_timeout(self.timeout)
-        return conn
+  def make_connection(self, host):
+    conn = TimeoutHTTP(host)
+    conn.set_timeout(self.timeout)
+    return conn
 
 class TimeoutXMLRPCServerProxy(xmlrpclib.ServerProxy):
-    '''XML/RPC ServerProxy supporting timeout.'''
-    def __init__(self, uri, timeout=10, *args, **kwargs):
-        if timeout:
-            kwargs['transport'] = TimeoutXMLRPCTransport(
-                timeout=timeout)
-        xmlrpclib.ServerProxy.__init__(self, uri, *args, **kwargs)
+  '''XML/RPC ServerProxy supporting timeout.'''
+  def __init__(self, uri, timeout=10, *args, **kwargs):
+    if timeout:
+      kwargs['transport'] = TimeoutXMLRPCTransport(
+        timeout=timeout)
+    xmlrpclib.ServerProxy.__init__(self, uri, *args, **kwargs)
diff --git a/py/utils/net_utils_unittest.py b/py/utils/net_utils_unittest.py
index 460d3d1..047e786 100644
--- a/py/utils/net_utils_unittest.py
+++ b/py/utils/net_utils_unittest.py
@@ -4,53 +4,56 @@
 
 """Networking-related utilities."""
 
-import random
 import SimpleXMLRPCServer
 import socket
 import threading
 import time
 import unittest
 
-import factory_common
+import factory_common  # pylint: disable=W0611
 from cros.factory.utils import net_utils
 from cros.factory.utils import test_utils
 
 
 class TimeoutXMLRPCTest(unittest.TestCase):
-    def setUp(self):
-        self.port = test_utils.FindUnusedTCPPort()
-        self.server = SimpleXMLRPCServer.SimpleXMLRPCServer(
-            ('localhost', self.port),
-            allow_none=True)
-        self.server.register_function(time.sleep)
-        self.thread = threading.Thread(target=self.server.serve_forever)
-        self.thread.daemon = True
-        self.thread.start()
+  def __init__(self, *args, **kwargs):
+    super(TimeoutXMLRPCTest, self).__init__(*args, **kwargs)
+    self.client = None
 
-    def tearDown(self):
-        self.server.shutdown()
+  def setUp(self):
+    self.port = test_utils.FindUnusedTCPPort()
+    self.server = SimpleXMLRPCServer.SimpleXMLRPCServer(
+      ('localhost', self.port),
+      allow_none=True)
+    self.server.register_function(time.sleep)
+    self.thread = threading.Thread(target=self.server.serve_forever)
+    self.thread.daemon = True
+    self.thread.start()
 
-    def MakeProxy(self, timeout):
-        return net_utils.TimeoutXMLRPCServerProxy(
-            'http://localhost:%d' % self.port, timeout=timeout, allow_none=True)
+  def tearDown(self):
+    self.server.shutdown()
 
-    def runTest(self):
-        self.client = self.MakeProxy(timeout=1)
+  def MakeProxy(self, timeout):
+    return net_utils.TimeoutXMLRPCServerProxy(
+      'http://localhost:%d' % self.port, timeout=timeout, allow_none=True)
 
-        start = time.time()
-        self.client.sleep(.001)  # No timeout
-        delta = time.time() - start
-        self.assertTrue(delta < 1, delta)
+  def runTest(self):
+    self.client = self.MakeProxy(timeout=1)
 
-        start = time.time()
-        try:
-            self.client.sleep(2)  # Cause a timeout in 1 s
-            self.fail('Expected exception')
-        except socket.timeout:
-            # Good!
-            delta = time.time() - start
-            self.assertTrue(delta > .25, delta)
-            self.assertTrue(delta < 2, delta)
+    start = time.time()
+    self.client.sleep(.001)  # No timeout
+    delta = time.time() - start
+    self.assertTrue(delta < 1, delta)
+
+    start = time.time()
+    try:
+      self.client.sleep(2)  # Cause a timeout in 1 s
+      self.fail('Expected exception')
+    except socket.timeout:
+      # Good!
+      delta = time.time() - start
+      self.assertTrue(delta > .25, delta)
+      self.assertTrue(delta < 2, delta)
 
 if __name__ == '__main__':
-    unittest.main()
+  unittest.main()
diff --git a/py/utils/test_utils.py b/py/utils/test_utils.py
index 1593d77..4c36b85 100644
--- a/py/utils/test_utils.py
+++ b/py/utils/test_utils.py
@@ -9,8 +9,8 @@
 
 
 def FindUnusedTCPPort():
-    '''Returns an unused TCP port for testing.
+  '''Returns an unused TCP port for testing.
 
-    Currently just returns a random port from [10000,20000).
-    '''
-    return random.randint(10000, 19999)
+  Currently just returns a random port from [10000,20000).
+  '''
+  return random.randint(10000, 19999)