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)