summaryrefslogtreecommitdiffstats
path: root/testing/marionette/client/marionette_driver/wait.py
blob: bc34ccf4cbd81f4b61d2d6245fe39f713f2ccf48 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# 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()