Home | History | Annotate | Download | only in cros
      1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import gobject, logging, sys, traceback
      6 
      7 import common
      8 from autotest_lib.client.common_lib import error
      9 
     10 # TODO(rochberg): Take another shot at fixing glib to allow this
     11 # behavior when desired
     12 def ExceptionForward(func):
     13   """Decorator that saves exceptions for forwarding across a glib
     14   mainloop.
     15 
     16   Exceptions thrown by glib callbacks are swallowed if they reach the
     17   glib main loop. This decorator collaborates with
     18   ExceptionForwardingMainLoop to save those exceptions so that it can
     19   reraise them."""
     20   def wrapper(self, *args, **kwargs):
     21     try:
     22       return func(self, *args, **kwargs)
     23     except Exception, e:
     24       logging.warning('Saving exception: %s' % e)
     25       logging.warning(''.join(traceback.format_exception(*sys.exc_info())))
     26       self._forwarded_exception = e
     27       self.main_loop.quit()
     28       return False
     29   return wrapper
     30 
     31 class ExceptionForwardingMainLoop(object):
     32   """Wraps a glib mainloop so that exceptions raised by functions
     33   called by the mainloop cause the mainloop to terminate and reraise
     34   the exception.
     35 
     36   Any function called by the main loop (including dbus callbacks and
     37   glib callbacks like add_idle) must be wrapped in the
     38   @ExceptionForward decorator."""
     39 
     40   def __init__(self, main_loop, timeout_s=-1):
     41     self._forwarded_exception = None
     42     self.main_loop = main_loop
     43     if timeout_s == -1:
     44       logging.warning('ExceptionForwardingMainLoop: No timeout specified.')
     45       logging.warning('(Specify timeout_s=0 explicitly for no timeout.)')
     46     self.timeout_s = timeout_s
     47 
     48   def idle(self):
     49     raise Exception('idle must be overridden')
     50 
     51   def timeout(self):
     52     pass
     53 
     54   @ExceptionForward
     55   def _timeout(self):
     56     self.timeout()
     57     raise error.TestFail('main loop timed out')
     58 
     59   def quit(self):
     60     self.main_loop.quit()
     61 
     62   def run(self):
     63     gobject.idle_add(self.idle)
     64     if self.timeout_s > 0:
     65       timeout_source = gobject.timeout_add(self.timeout_s * 1000, self._timeout)
     66     self.main_loop.run()
     67     if self.timeout_s > 0:
     68       gobject.source_remove(timeout_source)
     69 
     70     if self._forwarded_exception:
     71       raise self._forwarded_exception
     72 
     73 class GenericTesterMainLoop(ExceptionForwardingMainLoop):
     74   """Runs a glib mainloop until it times out or all requirements are
     75   satisfied."""
     76 
     77   def __init__(self, test, main_loop, **kwargs):
     78     super(GenericTesterMainLoop, self).__init__(main_loop, **kwargs)
     79     self.test = test
     80     self.property_changed_actions = {}
     81 
     82   def idle(self):
     83     self.perform_one_test()
     84 
     85   def perform_one_test(self):
     86     """Subclasses override this function to do their testing."""
     87     raise Exception('perform_one_test must be overridden')
     88 
     89   def after_main_loop(self):
     90     """Children can override this to clean up after the main loop."""
     91     pass
     92 
     93   def build_error_handler(self, name):
     94     """Returns a closure that fails the test with the specified name."""
     95     @ExceptionForward
     96     def to_return(self, e):
     97       raise error.TestFail('Dbus call %s failed: %s' % (name, e))
     98     # Bind the returned handler function to this object
     99     return to_return.__get__(self, GenericTesterMainLoop)
    100 
    101   @ExceptionForward
    102   def ignore_handler(*ignored_args, **ignored_kwargs):
    103     pass
    104 
    105   def requirement_completed(self, requirement, warn_if_already_completed=True):
    106     """Record that a requirement was completed.  Exit if all are."""
    107     should_log = True
    108     try:
    109       self.remaining_requirements.remove(requirement)
    110     except KeyError:
    111       if warn_if_already_completed:
    112         logging.warning('requirement %s was not present to be completed',
    113                         requirement)
    114       else:
    115         should_log = False
    116 
    117     if not self.remaining_requirements:
    118       logging.info('All requirements satisfied')
    119       self.quit()
    120     else:
    121       if should_log:
    122         logging.info('Requirement %s satisfied.  Remaining: %s' %
    123                      (requirement, self.remaining_requirements))
    124 
    125   def timeout(self):
    126     logging.error('Requirements unsatisfied upon timeout: %s' %
    127                     self.remaining_requirements)
    128 
    129   @ExceptionForward
    130   def dispatch_property_changed(self, property, *args, **kwargs):
    131     action = self.property_changed_actions.pop(property, None)
    132     if action:
    133       logging.info('Property_changed dispatching %s' % property)
    134       action(property, *args, **kwargs)
    135 
    136   def assert_(self, arg):
    137     self.test.assert_(self, arg)
    138 
    139   def run(self, *args, **kwargs):
    140     self.test_args = args
    141     self.test_kwargs = kwargs
    142     ExceptionForwardingMainLoop.run(self)
    143     self.after_main_loop()
    144