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 5import gobject, logging, sys, traceback 6 7import common 8from autotest_lib.client.common_lib import error 9 10# TODO(rochberg): Take another shot at fixing glib to allow this 11# behavior when desired 12def 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 31class 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 73class 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