1""" 2This module contains the actions that a configurable CFM test can execute. 3""" 4import abc 5import logging 6import random 7import re 8import sys 9import time 10 11class Action(object): 12 """ 13 Abstract base class for all actions. 14 """ 15 __metaclass__ = abc.ABCMeta 16 17 def __repr__(self): 18 return self.__class__.__name__ 19 20 def execute(self, context): 21 """ 22 Executes the action. 23 24 @param context ActionContext instance providing dependencies to the 25 action. 26 """ 27 logging.info('Executing action "%s"', self) 28 self.do_execute(context) 29 logging.info('Done executing action "%s"', self) 30 31 @abc.abstractmethod 32 def do_execute(self, context): 33 """ 34 Performs the actual execution. 35 36 Subclasses must override this method. 37 38 @param context ActionContext instance providing dependencies to the 39 action. 40 """ 41 pass 42 43class MuteMicrophone(Action): 44 """ 45 Mutes the microphone in a call. 46 """ 47 def do_execute(self, context): 48 context.cfm_facade.mute_mic() 49 50class UnmuteMicrophone(Action): 51 """ 52 Unmutes the microphone in a call. 53 """ 54 def do_execute(self, context): 55 context.cfm_facade.unmute_mic() 56 57class WaitForMeetingsLandingPage(Action): 58 """ 59 Wait for landing page to load after reboot. 60 """ 61 def do_execute(self, context): 62 context.cfm_facade.wait_for_meetings_landing_page() 63 64class JoinMeeting(Action): 65 """ 66 Joins a meeting. 67 """ 68 def __init__(self, meeting_code): 69 """ 70 Initializes. 71 72 @param meeting_code The meeting code for the meeting to join. 73 """ 74 super(JoinMeeting, self).__init__() 75 self.meeting_code = meeting_code 76 77 def __repr__(self): 78 return 'JoinMeeting "%s"' % self.meeting_code 79 80 def do_execute(self, context): 81 context.cfm_facade.join_meeting_session(self.meeting_code) 82 83class CreateMeeting(Action): 84 """ 85 Creates a new meeting from the landing page. 86 """ 87 def do_execute(self, context): 88 context.cfm_facade.start_meeting_session() 89 90class LeaveMeeting(Action): 91 """ 92 Leaves the current meeting. 93 """ 94 def do_execute(self, context): 95 context.cfm_facade.end_meeting_session() 96 97class RebootDut(Action): 98 """ 99 Reboots the DUT. 100 """ 101 def __init__(self, restart_chrome_for_cfm=False): 102 """Initializes. 103 104 To enable the cfm_facade to interact with the CFM, Chrome needs an extra 105 restart. Setting restart_chrome_for_cfm toggles this extra restart. 106 107 @param restart_chrome_for_cfm If True, restarts chrome to enable 108 the cfm_facade and waits for the telemetry commands to become 109 available. If false, does not do an extra restart of Chrome. 110 """ 111 self._restart_chrome_for_cfm = restart_chrome_for_cfm 112 113 def do_execute(self, context): 114 context.host.reboot() 115 if self._restart_chrome_for_cfm: 116 context.cfm_facade.restart_chrome_for_cfm() 117 context.cfm_facade.wait_for_meetings_telemetry_commands() 118 119class RepeatTimes(Action): 120 """ 121 Repeats a scenario a number of times. 122 """ 123 def __init__(self, times, scenario): 124 """ 125 Initializes. 126 127 @param times The number of times to repeat the scenario. 128 @param scenario The scenario to repeat. 129 """ 130 super(RepeatTimes, self).__init__() 131 self.times = times 132 self.scenario = scenario 133 134 def __str__(self): 135 return 'Repeat[scenario=%s, times=%s]' % (self.scenario, self.times) 136 137 def do_execute(self, context): 138 for _ in xrange(self.times): 139 self.scenario.execute(context) 140 141class AssertFileDoesNotContain(Action): 142 """ 143 Asserts that a file on the DUT does not contain specified regexes. 144 """ 145 def __init__(self, path, forbidden_regex_list): 146 """ 147 Initializes. 148 149 @param path The file path on the DUT to check. 150 @param forbidden_regex_list a list with regular expressions that should 151 not appear in the file. 152 """ 153 super(AssertFileDoesNotContain, self).__init__() 154 self.path = path 155 self.forbidden_regex_list = forbidden_regex_list 156 157 def __repr__(self): 158 return ('AssertFileDoesNotContain[path=%s, forbidden_regex_list=%s' 159 % (self.path, self.forbidden_regex_list)) 160 161 def do_execute(self, context): 162 contents = context.file_contents_collector.collect_file_contents( 163 self.path) 164 for forbidden_regex in self.forbidden_regex_list: 165 match = re.search(forbidden_regex, contents) 166 if match: 167 raise AssertionError( 168 'Regex "%s" matched "%s" in "%s"' 169 % (forbidden_regex, match.group(), self.path)) 170 171class AssertUsbDevices(Action): 172 """ 173 Asserts that USB devices with given specs matches a predicate. 174 """ 175 def __init__( 176 self, 177 usb_device_specs, 178 predicate=lambda usb_device_list: len(usb_device_list) == 1): 179 """ 180 Initializes with a spec to assert and a predicate. 181 182 @param usb_device_specs a list of UsbDeviceSpecs for the devices to 183 check. 184 @param predicate A function that accepts a list of UsbDevices 185 and returns true if the list is as expected or false otherwise. 186 If the method returns false an AssertionError is thrown. 187 The default predicate checks that there is exactly one item 188 in the list. 189 """ 190 super(AssertUsbDevices, self).__init__() 191 self._usb_device_specs = usb_device_specs 192 self._predicate = predicate 193 194 def do_execute(self, context): 195 usb_devices = context.usb_device_collector.get_devices_by_spec( 196 *self._usb_device_specs) 197 if not self._predicate(usb_devices): 198 raise AssertionError( 199 'Assertion failed for usb device specs %s. ' 200 'Usb devices were: %s' 201 % (self._usb_device_specs, usb_devices)) 202 203 def __str__(self): 204 return 'AssertUsbDevices for specs %s' % str(self._usb_device_specs) 205 206class SelectScenarioAtRandom(Action): 207 """ 208 Executes a randomly selected scenario a number of times. 209 210 Note that there is no validation performed - you have to take care 211 so that it makes sense to execute the supplied scenarios in any order 212 any number of times. 213 """ 214 def __init__( 215 self, 216 scenarios, 217 run_times, 218 random_seed=random.randint(0, sys.maxsize)): 219 """ 220 Initializes. 221 222 @param scenarios An iterable with scenarios to choose from. 223 @param run_times The number of scenarios to run. I.e. the number of 224 times a random scenario is selected. 225 @param random_seed The seed to use for the random generator. Providing 226 the same seed as an earlier run will execute the scenarios in the 227 same order. Optional, by default a random seed is used. 228 """ 229 super(SelectScenarioAtRandom, self).__init__() 230 self._scenarios = scenarios 231 self._run_times = run_times 232 self._random_seed = random_seed 233 self._random = random.Random(random_seed) 234 235 def do_execute(self, context): 236 for _ in xrange(self._run_times): 237 self._random.choice(self._scenarios).execute(context) 238 239 def __repr__(self): 240 return ('SelectScenarioAtRandom [seed=%s, run_times=%s, scenarios=%s]' 241 % (self._random_seed, self._run_times, self._scenarios)) 242 243 244class PowerCycleUsbPort(Action): 245 """ 246 Power cycle USB ports that a specific peripheral type is attached to. 247 """ 248 def __init__( 249 self, 250 usb_device_specs, 251 wait_for_change_timeout=10, 252 filter_function=lambda x: x): 253 """ 254 Initializes. 255 256 @param usb_device_specs List of UsbDeviceSpecs of the devices to power 257 cycle the port for. 258 @param wait_for_change_timeout The timeout in seconds for waiting 259 for devices to disappeard/appear after turning power off/on. 260 If the devices do not disappear/appear within the timeout an 261 error is raised. 262 @param filter_function Function accepting a list of UsbDevices and 263 returning a list of UsbDevices that should be power cycled. The 264 default is to return the original list, i.e. power cycle all 265 devices matching the usb_device_specs. 266 267 @raises TimeoutError if the devices do not turn off/on within 268 wait_for_change_timeout seconds. 269 """ 270 self._usb_device_specs = usb_device_specs 271 self._filter_function = filter_function 272 self._wait_for_change_timeout = wait_for_change_timeout 273 274 def do_execute(self, context): 275 def _get_devices(): 276 return context.usb_device_collector.get_devices_by_spec( 277 *self._usb_device_specs) 278 devices = _get_devices() 279 devices_to_cycle = self._filter_function(devices) 280 # If we are asked to power cycle a device connected to a USB hub (for 281 # example a Mimo which has an internal hub) the devices's bus and port 282 # cannot be used. Those values represent the bus and port of the hub. 283 # Instead we must locate the device that is actually connected to the 284 # physical USB port. This device is the parent at level 1 of the current 285 # device. If the device is not connected to a hub, device.get_parent(1) 286 # will return the device itself. 287 devices_to_cycle = [device.get_parent(1) for device in devices_to_cycle] 288 logging.debug('Power cycling devices: %s', devices_to_cycle) 289 port_ids = [(d.bus, d.port) for d in devices_to_cycle] 290 context.usb_port_manager.set_port_power(port_ids, False) 291 # TODO(kerl): We should do a better check than counting devices. 292 # Possibly implementing __eq__() in UsbDevice and doing a proper 293 # intersection to see which devices are running or not. 294 expected_devices_after_power_off = len(devices) - len(devices_to_cycle) 295 _wait_for_condition( 296 lambda: len(_get_devices()) == expected_devices_after_power_off, 297 self._wait_for_change_timeout) 298 context.usb_port_manager.set_port_power(port_ids, True) 299 _wait_for_condition( 300 lambda: len(_get_devices()) == len(devices), 301 self._wait_for_change_timeout) 302 303 def __repr__(self): 304 return ('PowerCycleUsbPort[usb_device_specs=%s, ' 305 'wait_for_change_timeout=%s]' 306 % (str(self._usb_device_specs), self._wait_for_change_timeout)) 307 308 309class Sleep(Action): 310 """ 311 Action that sleeps for a number of seconds. 312 """ 313 def __init__(self, num_seconds): 314 """ 315 Initializes. 316 317 @param num_seconds The number of seconds to sleep. 318 """ 319 self._num_seconds = num_seconds 320 321 def do_execute(self, context): 322 time.sleep(self._num_seconds) 323 324 def __repr__(self): 325 return 'Sleep[num_seconds=%s]' % self._num_seconds 326 327 328class RetryAssertAction(Action): 329 """ 330 Action that retries an assertion action a number of times if it fails. 331 332 An example use case for this action is to verify that a peripheral device 333 appears after power cycling. E.g.: 334 PowerCycleUsbPort(ATRUS), 335 RetryAssertAction(AssertUsbDevices(ATRUS), 10) 336 """ 337 def __init__(self, action, num_tries, retry_delay_seconds=1): 338 """ 339 Initializes. 340 341 @param action The action to execute. 342 @param num_tries The number of times to try the action before failing 343 for real. Must be more than 0. 344 @param retry_delay_seconds The number of seconds to sleep between 345 retries. 346 347 @raises ValueError if num_tries is below 1. 348 """ 349 super(RetryAssertAction, self).__init__() 350 if num_tries < 1: 351 raise ValueError('num_tries must be > 0. Was %s' % num_tries) 352 self._action = action 353 self._num_tries = num_tries 354 self._retry_delay_seconds = retry_delay_seconds 355 356 def do_execute(self, context): 357 for attempt in xrange(self._num_tries): 358 try: 359 self._action.execute(context) 360 return 361 except AssertionError as e: 362 if attempt == self._num_tries - 1: 363 raise e 364 else: 365 logging.info( 366 'Action %s failed, will retry %d more times', 367 self._action, 368 self._num_tries - attempt - 1, 369 exc_info=True) 370 time.sleep(self._retry_delay_seconds) 371 372 def __repr__(self): 373 return ('RetryAssertAction[action=%s, ' 374 'num_tries=%s, retry_delay_seconds=%s]' 375 % (self._action, self._num_tries, self._retry_delay_seconds)) 376 377 378class AssertNoNewCrashes(Action): 379 """ 380 Asserts that no new crash files exist on disk. 381 """ 382 def do_execute(self, context): 383 new_crash_files = context.crash_detector.get_new_crash_files() 384 if new_crash_files: 385 raise AssertionError( 386 'New crash files detected: %s' % str(new_crash_files)) 387 388 389class TimeoutError(RuntimeError): 390 """ 391 Error raised when an operation times out. 392 """ 393 pass 394 395 396def _wait_for_condition(condition, timeout_seconds=10): 397 """ 398 Wait for a condition to become true. 399 400 Checks the condition every second. 401 402 @param condition The condition to check - a function returning a boolean. 403 @param timeout_seconds The timeout in seconds. 404 405 @raises TimeoutError in case the condition does not become true within 406 the timeout. 407 """ 408 if condition(): 409 return 410 for _ in xrange(timeout_seconds): 411 time.sleep(1) 412 if condition(): 413 return 414 raise TimeoutError('Timeout after %s seconds waiting for condition %s' 415 % (timeout_seconds, condition)) 416 417 418class StartPerfMetricsCollection(Action): 419 """ 420 Starts collecting performance data. 421 422 Collection is performed in a background thread so this operation returns 423 immediately. 424 425 This action only collects the data, it does not upload it. 426 Use UploadPerfMetrics to upload the data to the perf dashboard. 427 """ 428 def do_execute(self, context): 429 context.perf_metrics_collector.start() 430 431 432class StopPerfMetricsCollection(Action): 433 """ 434 Stops collecting performance data. 435 436 This action only stops collecting the data, it does not upload it. 437 Use UploadPerfMetrics to upload the data to the perf dashboard. 438 """ 439 def do_execute(self, context): 440 context.perf_metrics_collector.stop() 441 442 443class UploadPerfMetrics(Action): 444 """ 445 Uploads the collected perf metrics to the perf dashboard. 446 """ 447 def do_execute(self, context): 448 context.perf_metrics_collector.upload_metrics() 449 450 451class CreateMeetingWithBots(Action): 452 """ 453 Creates a new meeting prepopulated with bots. 454 455 Call JoinMeetingWithBots() do join it with a CfM. 456 """ 457 def __init__(self, bot_count, bots_ttl_min, muted=True): 458 """ 459 Initializes. 460 461 @param bot_count Amount of bots to be in the meeting. 462 @param bots_ttl_min TTL in minutes after which the bots leave. 463 @param muted If the bots are audio muted or not. 464 """ 465 super(CreateMeetingWithBots, self).__init__() 466 self._bot_count = bot_count 467 # Adds an extra 30 seconds buffer 468 self._bots_ttl_sec = bots_ttl_min * 60 + 30 469 self._muted = muted 470 471 def __repr__(self): 472 return ( 473 'CreateMeetingWithBots:\n' 474 ' bot_count: %d\n' 475 ' bots_ttl_sec: %d\n' 476 ' muted: %s' % (self._bot_count, self._bots_ttl_sec, self._muted) 477 ) 478 479 def do_execute(self, context): 480 if context.bots_meeting_code: 481 raise AssertionError( 482 'A meeting with bots is already running. ' 483 'Repeated calls to CreateMeetingWithBots() are not supported.') 484 context.bots_meeting_code = context.bond_api.CreateConference() 485 context.bond_api.AddBotsRequest( 486 context.bots_meeting_code, 487 self._bot_count, 488 self._bots_ttl_sec); 489 mute_cmd = 'mute_audio' if self._muted else 'unmute_audio' 490 context.bond_api.ExecuteScript('@all %s' % mute_cmd, 491 context.bots_meeting_code) 492 493 494class JoinMeetingWithBots(Action): 495 """ 496 Joins an existing meeting started via CreateMeetingWithBots(). 497 """ 498 def do_execute(self, context): 499 meeting_code = context.bots_meeting_code 500 if not meeting_code: 501 raise AssertionError( 502 'Meeting with bots was not started. ' 503 'Did you forget to call CreateMeetingWithBots()?') 504 context.cfm_facade.join_meeting_session(context.bots_meeting_code) 505