# 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()