• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2022 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Module for the base class to handle Mobly Snippet Lib's callback events."""
15import abc
16import time
17
18from mobly.snippet import callback_event
19from mobly.snippet import errors
20
21
22class CallbackHandlerBase(abc.ABC):
23  """Base class for handling Mobly Snippet Lib's callback events.
24
25  All the events handled by a callback handler are originally triggered by one
26  async RPC call. All the events are tagged with a callback_id specific to a
27  call to an async RPC method defined on the server side.
28
29  The raw message representing an event looks like:
30
31  .. code-block:: python
32
33    {
34      'callbackId': <string, callbackId>,
35      'name': <string, name of the event>,
36      'time': <long, epoch time of when the event was created on the
37        server side>,
38      'data': <dict, extra data from the callback on the server side>
39    }
40
41  Each message is then used to create a CallbackEvent object on the client
42  side.
43
44  Attributes:
45    ret_value: any, the direct return value of the async RPC call.
46  """
47
48  def __init__(self,
49               callback_id,
50               event_client,
51               ret_value,
52               method_name,
53               device,
54               rpc_max_timeout_sec,
55               default_timeout_sec=120):
56    """Initializes a callback handler base object.
57
58    Args:
59      callback_id: str, the callback ID which associates with a group of
60        callback events.
61      event_client: SnippetClientV2, the client object used to send RPC to the
62        server and receive response.
63      ret_value: any, the direct return value of the async RPC call.
64      method_name: str, the name of the executed Async snippet function.
65      device: DeviceController, the device object associated with this handler.
66      rpc_max_timeout_sec: float, maximum time for sending a single RPC call.
67      default_timeout_sec: float, the default timeout for this handler. It
68        must be no longer than rpc_max_timeout_sec.
69    """
70    self._id = callback_id
71    self.ret_value = ret_value
72    self._device = device
73    self._event_client = event_client
74    self._method_name = method_name
75
76    if rpc_max_timeout_sec < default_timeout_sec:
77      raise ValueError('The max timeout of a single RPC must be no smaller '
78                       'than the default timeout of the callback handler. '
79                       f'Got rpc_max_timeout_sec={rpc_max_timeout_sec}, '
80                       f'default_timeout_sec={default_timeout_sec}.')
81    self._rpc_max_timeout_sec = rpc_max_timeout_sec
82    self._default_timeout_sec = default_timeout_sec
83
84  @property
85  def rpc_max_timeout_sec(self):
86    """Maximum time for sending a single RPC call."""
87    return self._rpc_max_timeout_sec
88
89  @property
90  def default_timeout_sec(self):
91    """Default timeout used by this callback handler."""
92    return self._default_timeout_sec
93
94  @property
95  def callback_id(self):
96    """The callback ID which associates a group of callback events."""
97    return self._id
98
99  @abc.abstractmethod
100  def callEventWaitAndGetRpc(self, callback_id, event_name, timeout_sec):
101    """Calls snippet lib's RPC to wait for a callback event.
102
103    Override this method to use this class with various snippet lib
104    implementations.
105
106    This function waits and gets a CallbackEvent with the specified identifier
107    from the server. It will raise a timeout error if the expected event does
108    not occur within the time limit.
109
110    Args:
111      callback_id: str, the callback identifier.
112      event_name: str, the callback name.
113      timeout_sec: float, the number of seconds to wait for the event. It is
114        already checked that this argument is no longer than the max timeout
115        of a single RPC.
116
117    Returns:
118      The event dictionary.
119
120    Raises:
121      errors.CallbackHandlerTimeoutError: Raised if the expected event does not
122        occur within the time limit.
123    """
124
125  @abc.abstractmethod
126  def callEventGetAllRpc(self, callback_id, event_name):
127    """Calls snippet lib's RPC to get all existing snippet events.
128
129    Override this method to use this class with various snippet lib
130    implementations.
131
132    This function gets all existing events in the server with the specified
133    identifier without waiting.
134
135    Args:
136      callback_id: str, the callback identifier.
137      event_name: str, the callback name.
138
139    Returns:
140      A list of event dictionaries.
141    """
142
143  def waitAndGet(self, event_name, timeout=None):
144    """Waits and gets a CallbackEvent with the specified identifier.
145
146    It will raise a timeout error if the expected event does not occur within
147    the time limit.
148
149    Args:
150      event_name: str, the name of the event to get.
151      timeout: float, the number of seconds to wait before giving up. If None,
152        it will be set to self.default_timeout_sec.
153
154    Returns:
155      CallbackEvent, the oldest entry of the specified event.
156
157    Raises:
158      errors.CallbackHandlerBaseError: If the specified timeout is longer than
159        the max timeout supported.
160      errors.CallbackHandlerTimeoutError: The expected event does not occur
161        within the time limit.
162    """
163    if timeout is None:
164      timeout = self.default_timeout_sec
165
166    if timeout:
167      if timeout > self.rpc_max_timeout_sec:
168        raise errors.CallbackHandlerBaseError(
169            self._device,
170            f'Specified timeout {timeout} is longer than max timeout '
171            f'{self.rpc_max_timeout_sec}.')
172
173    raw_event = self.callEventWaitAndGetRpc(self._id, event_name, timeout)
174    return callback_event.from_dict(raw_event)
175
176  def waitForEvent(self, event_name, predicate, timeout=None):
177    """Waits for an event of the specific name that satisfies the predicate.
178
179    This call will block until the expected event has been received or time
180    out.
181
182    The predicate function defines the condition the event is expected to
183    satisfy. It takes an event and returns True if the condition is
184    satisfied, False otherwise.
185
186    Note all events of the same name that are received but don't satisfy
187    the predicate will be discarded and not be available for further
188    consumption.
189
190    Args:
191      event_name: str, the name of the event to wait for.
192      predicate: function, a function that takes an event (dictionary) and
193        returns a bool.
194      timeout: float, the number of seconds to wait before giving up. If None,
195        it will be set to self.default_timeout_sec.
196
197    Returns:
198      dictionary, the event that satisfies the predicate if received.
199
200    Raises:
201      errors.CallbackHandlerTimeoutError: raised if no event that satisfies the
202        predicate is received after timeout seconds.
203    """
204    if timeout is None:
205      timeout = self.default_timeout_sec
206
207    deadline = time.perf_counter() + timeout
208    while time.perf_counter() <= deadline:
209      single_rpc_timeout = deadline - time.perf_counter()
210      if single_rpc_timeout < 0:
211        break
212
213      single_rpc_timeout = min(single_rpc_timeout, self.rpc_max_timeout_sec)
214      try:
215        event = self.waitAndGet(event_name, single_rpc_timeout)
216      except errors.CallbackHandlerTimeoutError:
217        # Ignoring errors.CallbackHandlerTimeoutError since we need to throw
218        # one with a more specific message.
219        break
220      if predicate(event):
221        return event
222
223    raise errors.CallbackHandlerTimeoutError(
224        self._device,
225        f'Timed out after {timeout}s waiting for an "{event_name}" event that '
226        f'satisfies the predicate "{predicate.__name__}".')
227
228  def getAll(self, event_name):
229    """Gets all existing events in the server with the specified identifier.
230
231    This is a non-blocking call.
232
233    Args:
234      event_name: str, the name of the event to get.
235
236    Returns:
237      A list of CallbackEvent, each representing an event from the Server side.
238    """
239    raw_events = self.callEventGetAllRpc(self._id, event_name)
240    return [callback_event.from_dict(msg) for msg in raw_events]
241