Home | History | Annotate | Download | only in configurable_test
      1 """
      2 This module contains the actions that a configurable CFM test can execute.
      3 """
      4 import abc
      5 import logging
      6 import random
      7 import re
      8 import sys
      9 import time
     10 
     11 class 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 
     43 class MuteMicrophone(Action):
     44     """
     45     Mutes the microphone in a call.
     46     """
     47     def do_execute(self, context):
     48         context.cfm_facade.mute_mic()
     49 
     50 class UnmuteMicrophone(Action):
     51     """
     52     Unmutes the microphone in a call.
     53     """
     54     def do_execute(self, context):
     55         context.cfm_facade.unmute_mic()
     56 
     57 class 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 
     64 class 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 
     83 class 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 
     90 class LeaveMeeting(Action):
     91     """
     92     Leaves the current meeting.
     93     """
     94     def do_execute(self, context):
     95         context.cfm_facade.end_meeting_session()
     96 
     97 class 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 
    119 class 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 
    141 class 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 
    171 class 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 
    206 class 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 
    244 class 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 
    309 class 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 
    328 class 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 
    378 class 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 
    389 class TimeoutError(RuntimeError):
    390     """
    391     Error raised when an operation times out.
    392     """
    393     pass
    394 
    395 
    396 def _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 
    418 class 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 
    432 class 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 
    443 class 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 
    451 class 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 
    494 class 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