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