wait.py (6140B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 import collections 6 import sys 7 import time 8 9 from . import errors 10 11 DEFAULT_TIMEOUT = 5 12 DEFAULT_INTERVAL = 0.1 13 14 15 class Wait: 16 """An explicit conditional utility class for waiting until a condition 17 evaluates to true or not null. 18 19 This will repeatedly evaluate a condition in anticipation for a 20 truthy return value, or its timeout to expire, or its waiting 21 predicate to become true. 22 23 A `Wait` instance defines the maximum amount of time to wait for a 24 condition, as well as the frequency with which to check the 25 condition. Furthermore, the user may configure the wait to ignore 26 specific types of exceptions whilst waiting, such as 27 `errors.NoSuchElementException` when searching for an element on 28 the page. 29 30 """ 31 32 def __init__( 33 self, 34 marionette, 35 timeout=None, 36 interval=None, 37 ignored_exceptions=None, 38 clock=None, 39 ): 40 """Configure the Wait instance to have a custom timeout, interval, and 41 list of ignored exceptions. Optionally a different time 42 implementation than the one provided by the standard library 43 (time) can also be provided. 44 45 Sample usage:: 46 47 # Wait 30 seconds for window to open, checking for its presence once 48 # every 5 seconds. 49 wait = Wait(marionette, timeout=30, interval=5, 50 ignored_exceptions=errors.NoSuchWindowException) 51 window = wait.until(lambda m: m.switch_to_window(42)) 52 53 :param marionette: The input value to be provided to 54 conditions, usually a Marionette instance. 55 56 :param timeout: How long to wait for the evaluated condition 57 to become true. The default timeout is 58 `wait.DEFAULT_TIMEOUT`. 59 60 :param interval: How often the condition should be evaluated. 61 In reality the interval may be greater as the cost of 62 evaluating the condition function. If that is not the case the 63 interval for the next condition function call is shortend to keep 64 the original interval sequence as best as possible. 65 The default polling interval is `wait.DEFAULT_INTERVAL`. 66 67 :param ignored_exceptions: Ignore specific types of exceptions 68 whilst waiting for the condition. Any exceptions not 69 whitelisted will be allowed to propagate, terminating the 70 wait. 71 72 :param clock: Allows overriding the use of the runtime's 73 default time library. See `wait.SystemClock` for 74 implementation details. 75 76 """ 77 78 self.marionette = marionette 79 self.timeout = timeout if timeout is not None else DEFAULT_TIMEOUT 80 self.interval = interval if interval is not None else DEFAULT_INTERVAL 81 self.clock = clock or SystemClock() 82 self.end = self.clock.now + self.timeout 83 84 exceptions = [] 85 if ignored_exceptions is not None: 86 if isinstance(ignored_exceptions, collections.abc.Iterable): 87 exceptions.extend(iter(ignored_exceptions)) 88 else: 89 exceptions.append(ignored_exceptions) 90 self.exceptions = tuple(set(exceptions)) 91 92 def until(self, condition, is_true=None, message=""): 93 """Repeatedly runs condition until its return value evaluates to true, 94 or its timeout expires or the predicate evaluates to true. 95 96 This will poll at the given interval until the given timeout 97 is reached, or the predicate or conditions returns true. A 98 condition that returns null or does not evaluate to true will 99 fully elapse its timeout before raising an 100 `errors.TimeoutException`. 101 102 If an exception is raised in the condition function and it's 103 not ignored, this function will raise immediately. If the 104 exception is ignored, it will continue polling for the 105 condition until it returns successfully or a 106 `TimeoutException` is raised. 107 108 :param condition: A callable function whose return value will 109 be returned by this function if it evaluates to true. 110 111 :param is_true: An optional predicate that will terminate and 112 return when it evaluates to False. It should be a 113 function that will be passed clock and an end time. The 114 default predicate will terminate a wait when the clock 115 elapses the timeout. 116 117 :param message: An optional message to include in the 118 exception's message if this function times out. 119 120 """ 121 122 rv = None 123 last_exc = None 124 until = is_true or until_pred 125 start = self.clock.now 126 127 while not until(self.clock, self.end): 128 try: 129 next = self.clock.now + self.interval 130 rv = condition(self.marionette) 131 except (KeyboardInterrupt, SystemExit): 132 raise 133 except self.exceptions: 134 last_exc = sys.exc_info() 135 136 # Re-adjust the interval depending on how long the callback 137 # took to evaluate the condition 138 interval_new = max(next - self.clock.now, 0) 139 140 if not rv: 141 self.clock.sleep(interval_new) 142 continue 143 144 if rv is not None: 145 return rv 146 147 self.clock.sleep(interval_new) 148 149 if message: 150 message = f" with message: {message}" 151 else: 152 message = "" 153 154 elapsed = round(self.clock.now - start, 1) 155 raise errors.TimeoutException( 156 f"Timed out after {elapsed:.1f} seconds{message}", 157 cause=last_exc, 158 ) 159 160 161 def until_pred(clock, end): 162 return clock.now >= end 163 164 165 class SystemClock: 166 def __init__(self): 167 self._time = time 168 169 def sleep(self, duration): 170 self._time.sleep(duration) 171 172 @property 173 def now(self): 174 return self._time.time()