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