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