# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import collections import sys import time from . import errors DEFAULT_TIMEOUT = 5 DEFAULT_INTERVAL = 0.1 class Wait(object): """An explicit conditional utility class for waiting until a condition evaluates to true or not null. This will repeatedly evaluate a condition in anticipation for a truthy return value, or its timeout to expire, or its waiting predicate to become true. A `Wait` instance defines the maximum amount of time to wait for a condition, as well as the frequency with which to check the condition. Furthermore, the user may configure the wait to ignore specific types of exceptions whilst waiting, such as `errors.NoSuchElementException` when searching for an element on the page. """ def __init__( self, marionette, timeout=None, interval=None, ignored_exceptions=None, clock=None, ): """Configure the Wait instance to have a custom timeout, interval, and list of ignored exceptions. Optionally a different time implementation than the one provided by the standard library (time) can also be provided. Sample usage:: # Wait 30 seconds for window to open, checking for its presence once # every 5 seconds. wait = Wait(marionette, timeout=30, interval=5, ignored_exceptions=errors.NoSuchWindowException) window = wait.until(lambda m: m.switch_to_window(42)) :param marionette: The input value to be provided to conditions, usually a Marionette instance. :param timeout: How long to wait for the evaluated condition to become true. The default timeout is `wait.DEFAULT_TIMEOUT`. :param interval: How often the condition should be evaluated. In reality the interval may be greater as the cost of evaluating the condition function. If that is not the case the interval for the next condition function call is shortend to keep the original interval sequence as best as possible. The default polling interval is `wait.DEFAULT_INTERVAL`. :param ignored_exceptions: Ignore specific types of exceptions whilst waiting for the condition. Any exceptions not whitelisted will be allowed to propagate, terminating the wait. :param clock: Allows overriding the use of the runtime's default time library. See `wait.SystemClock` for implementation details. """ self.marionette = marionette self.timeout = timeout if timeout is not None else DEFAULT_TIMEOUT self.interval = interval if interval is not None else DEFAULT_INTERVAL self.clock = clock or SystemClock() self.end = self.clock.now + self.timeout exceptions = [] if ignored_exceptions is not None: if isinstance(ignored_exceptions, collections.abc.Iterable): exceptions.extend(iter(ignored_exceptions)) else: exceptions.append(ignored_exceptions) self.exceptions = tuple(set(exceptions)) def until(self, condition, is_true=None, message=""): """Repeatedly runs condition until its return value evaluates to true, or its timeout expires or the predicate evaluates to true. This will poll at the given interval until the given timeout is reached, or the predicate or conditions returns true. A condition that returns null or does not evaluate to true will fully elapse its timeout before raising an `errors.TimeoutException`. If an exception is raised in the condition function and it's not ignored, this function will raise immediately. If the exception is ignored, it will continue polling for the condition until it returns successfully or a `TimeoutException` is raised. :param condition: A callable function whose return value will be returned by this function if it evaluates to true. :param is_true: An optional predicate that will terminate and return when it evaluates to False. It should be a function that will be passed clock and an end time. The default predicate will terminate a wait when the clock elapses the timeout. :param message: An optional message to include in the exception's message if this function times out. """ rv = None last_exc = None until = is_true or until_pred start = self.clock.now while not until(self.clock, self.end): try: next = self.clock.now + self.interval rv = condition(self.marionette) except (KeyboardInterrupt, SystemExit): raise except self.exceptions: last_exc = sys.exc_info() # Re-adjust the interval depending on how long the callback # took to evaluate the condition interval_new = max(next - self.clock.now, 0) if not rv: self.clock.sleep(interval_new) continue if rv is not None: return rv self.clock.sleep(interval_new) if message: message = " with message: {}".format(message) raise errors.TimeoutException( # pylint: disable=W1633 "Timed out after {0:.1f} seconds{1}".format( float(round((self.clock.now - start), 1)), message if message else "" ), cause=last_exc, ) def until_pred(clock, end): return clock.now >= end class SystemClock(object): def __init__(self): self._time = time def sleep(self, duration): self._time.sleep(duration) @property def now(self): return self._time.time()