| """ |
| This module contains the actions that a configurable CFM test can execute. |
| """ |
| import abc |
| import logging |
| import random |
| import re |
| import sys |
| import time |
| |
| class Action(object): |
| """ |
| Abstract base class for all actions. |
| """ |
| __metaclass__ = abc.ABCMeta |
| |
| def __repr__(self): |
| return self.__class__.__name__ |
| |
| def execute(self, context): |
| """ |
| Executes the action. |
| |
| @param context ActionContext instance providing dependencies to the |
| action. |
| """ |
| logging.info('Executing action "%s"', self) |
| self.do_execute(context) |
| logging.info('Done executing action "%s"', self) |
| |
| @abc.abstractmethod |
| def do_execute(self, context): |
| """ |
| Performs the actual execution. |
| |
| Subclasses must override this method. |
| |
| @param context ActionContext instance providing dependencies to the |
| action. |
| """ |
| pass |
| |
| class MuteMicrophone(Action): |
| """ |
| Mutes the microphone in a call. |
| """ |
| def do_execute(self, context): |
| context.cfm_facade.mute_mic() |
| |
| class UnmuteMicrophone(Action): |
| """ |
| Unmutes the microphone in a call. |
| """ |
| def do_execute(self, context): |
| context.cfm_facade.unmute_mic() |
| |
| class WaitForMeetingsLandingPage(Action): |
| """ |
| Wait for landing page to load after reboot. |
| """ |
| def do_execute(self, context): |
| context.cfm_facade.wait_for_meetings_landing_page() |
| |
| class JoinMeeting(Action): |
| """ |
| Joins a meeting. |
| """ |
| def __init__(self, meeting_code): |
| """ |
| Initializes. |
| |
| @param meeting_code The meeting code for the meeting to join. |
| """ |
| super(JoinMeeting, self).__init__() |
| self.meeting_code = meeting_code |
| |
| def __repr__(self): |
| return 'JoinMeeting "%s"' % self.meeting_code |
| |
| def do_execute(self, context): |
| context.cfm_facade.join_meeting_session(self.meeting_code) |
| |
| class CreateMeeting(Action): |
| """ |
| Creates a new meeting from the landing page. |
| """ |
| def do_execute(self, context): |
| context.cfm_facade.start_meeting_session() |
| |
| class LeaveMeeting(Action): |
| """ |
| Leaves the current meeting. |
| """ |
| def do_execute(self, context): |
| context.cfm_facade.end_meeting_session() |
| |
| class RebootDut(Action): |
| """ |
| Reboots the DUT. |
| """ |
| def __init__(self, restart_chrome_for_cfm=False): |
| """Initializes. |
| |
| To enable the cfm_facade to interact with the CFM, Chrome needs an extra |
| restart. Setting restart_chrome_for_cfm toggles this extra restart. |
| |
| @param restart_chrome_for_cfm If True, restarts chrome to enable |
| the cfm_facade and waits for the telemetry commands to become |
| available. If false, does not do an extra restart of Chrome. |
| """ |
| self._restart_chrome_for_cfm = restart_chrome_for_cfm |
| |
| def do_execute(self, context): |
| context.host.reboot() |
| if self._restart_chrome_for_cfm: |
| context.cfm_facade.restart_chrome_for_cfm() |
| context.cfm_facade.wait_for_telemetry_commands() |
| |
| class RepeatTimes(Action): |
| """ |
| Repeats a scenario a number of times. |
| """ |
| def __init__(self, times, scenario): |
| """ |
| Initializes. |
| |
| @param times The number of times to repeat the scenario. |
| @param scenario The scenario to repeat. |
| """ |
| super(RepeatTimes, self).__init__() |
| self.times = times |
| self.scenario = scenario |
| |
| def __str__(self): |
| return 'Repeat[scenario=%s, times=%s]' % (self.scenario, self.times) |
| |
| def do_execute(self, context): |
| for _ in xrange(self.times): |
| self.scenario.execute(context) |
| |
| class AssertFileDoesNotContain(Action): |
| """ |
| Asserts that a file on the DUT does not contain specified regexes. |
| """ |
| def __init__(self, path, forbidden_regex_list): |
| """ |
| Initializes. |
| |
| @param path The file path on the DUT to check. |
| @param forbidden_regex_list a list with regular expressions that should |
| not appear in the file. |
| """ |
| super(AssertFileDoesNotContain, self).__init__() |
| self.path = path |
| self.forbidden_regex_list = forbidden_regex_list |
| |
| def __repr__(self): |
| return ('AssertFileDoesNotContain[path=%s, forbidden_regex_list=%s' |
| % (self.path, self.forbidden_regex_list)) |
| |
| def do_execute(self, context): |
| contents = context.file_contents_collector.collect_file_contents( |
| self.path) |
| for forbidden_regex in self.forbidden_regex_list: |
| match = re.search(forbidden_regex, contents) |
| if match: |
| raise AssertionError( |
| 'Regex "%s" matched "%s" in "%s"' |
| % (forbidden_regex, match.group(), self.path)) |
| |
| class AssertUsbDevices(Action): |
| """ |
| Asserts that USB devices with given specs matches a predicate. |
| """ |
| def __init__( |
| self, |
| usb_device_specs, |
| predicate=lambda usb_device_list: len(usb_device_list) == 1): |
| """ |
| Initializes with a spec to assert and a predicate. |
| |
| @param usb_device_specs a list of UsbDeviceSpecs for the devices to |
| check. |
| @param predicate A function that accepts a list of UsbDevices |
| and returns true if the list is as expected or false otherwise. |
| If the method returns false an AssertionError is thrown. |
| The default predicate checks that there is exactly one item |
| in the list. |
| """ |
| super(AssertUsbDevices, self).__init__() |
| self._usb_device_specs = usb_device_specs |
| self._predicate = predicate |
| |
| def do_execute(self, context): |
| usb_devices = context.usb_device_collector.get_devices_by_spec( |
| *self._usb_device_specs) |
| if not self._predicate(usb_devices): |
| raise AssertionError( |
| 'Assertion failed for usb device specs %s. ' |
| 'Usb devices were: %s' |
| % (self._usb_device_specs, usb_devices)) |
| |
| def __str__(self): |
| return 'AssertUsbDevices for specs %s' % str(self._usb_device_specs) |
| |
| class SelectScenarioAtRandom(Action): |
| """ |
| Executes a randomly selected scenario a number of times. |
| |
| Note that there is no validation performed - you have to take care |
| so that it makes sense to execute the supplied scenarios in any order |
| any number of times. |
| """ |
| def __init__( |
| self, |
| scenarios, |
| run_times, |
| random_seed=random.randint(0, sys.maxsize)): |
| """ |
| Initializes. |
| |
| @param scenarios An iterable with scenarios to choose from. |
| @param run_times The number of scenarios to run. I.e. the number of |
| times a random scenario is selected. |
| @param random_seed The seed to use for the random generator. Providing |
| the same seed as an earlier run will execute the scenarios in the |
| same order. Optional, by default a random seed is used. |
| """ |
| super(SelectScenarioAtRandom, self).__init__() |
| self._scenarios = scenarios |
| self._run_times = run_times |
| self._random_seed = random_seed |
| self._random = random.Random(random_seed) |
| |
| def do_execute(self, context): |
| for _ in xrange(self._run_times): |
| self._random.choice(self._scenarios).execute(context) |
| |
| def __repr__(self): |
| return ('SelectScenarioAtRandom [seed=%s, run_times=%s, scenarios=%s]' |
| % (self._random_seed, self._run_times, self._scenarios)) |
| |
| |
| class PowerCycleUsbPort(Action): |
| """ |
| Power cycle USB ports that a specific peripheral type is attached to. |
| """ |
| def __init__( |
| self, |
| usb_device_specs, |
| wait_for_change_timeout=10, |
| filter_function=lambda x: x): |
| """ |
| Initializes. |
| |
| @param usb_device_specs List of UsbDeviceSpecs of the devices to power |
| cycle the port for. |
| @param wait_for_change_timeout The timeout in seconds for waiting |
| for devices to disappeard/appear after turning power off/on. |
| If the devices do not disappear/appear within the timeout an |
| error is raised. |
| @param filter_function Function accepting a list of UsbDevices and |
| returning a list of UsbDevices that should be power cycled. The |
| default is to return the original list, i.e. power cycle all |
| devices matching the usb_device_specs. |
| |
| @raises TimeoutError if the devices do not turn off/on within |
| wait_for_change_timeout seconds. |
| """ |
| self._usb_device_specs = usb_device_specs |
| self._filter_function = filter_function |
| self._wait_for_change_timeout = wait_for_change_timeout |
| |
| def do_execute(self, context): |
| def _get_devices(): |
| return context.usb_device_collector.get_devices_by_spec( |
| *self._usb_device_specs) |
| devices = _get_devices() |
| devices_to_cycle = self._filter_function(devices) |
| # If we are asked to power cycle a device connected to a USB hub (for |
| # example a Mimo which has an internal hub) the devices's bus and port |
| # cannot be used. Those values represent the bus and port of the hub. |
| # Instead we must locate the device that is actually connected to the |
| # physical USB port. This device is the parent at level 1 of the current |
| # device. If the device is not connected to a hub, device.get_parent(1) |
| # will return the device itself. |
| devices_to_cycle = [device.get_parent(1) for device in devices_to_cycle] |
| logging.debug('Power cycling devices: %s', devices_to_cycle) |
| port_ids = [(d.bus, d.port) for d in devices_to_cycle] |
| context.usb_port_manager.set_port_power(port_ids, False) |
| # TODO(kerl): We should do a better check than counting devices. |
| # Possibly implementing __eq__() in UsbDevice and doing a proper |
| # intersection to see which devices are running or not. |
| expected_devices_after_power_off = len(devices) - len(devices_to_cycle) |
| _wait_for_condition( |
| lambda: len(_get_devices()) == expected_devices_after_power_off, |
| self._wait_for_change_timeout) |
| context.usb_port_manager.set_port_power(port_ids, True) |
| _wait_for_condition( |
| lambda: len(_get_devices()) == len(devices), |
| self._wait_for_change_timeout) |
| |
| def __repr__(self): |
| return ('PowerCycleUsbPort[usb_device_specs=%s, ' |
| 'wait_for_change_timeout=%s]' |
| % (str(self._usb_device_specs), self._wait_for_change_timeout)) |
| |
| |
| class Sleep(Action): |
| """ |
| Action that sleeps for a number of seconds. |
| """ |
| def __init__(self, num_seconds): |
| """ |
| Initializes. |
| |
| @param num_seconds The number of seconds to sleep. |
| """ |
| self._num_seconds = num_seconds |
| |
| def do_execute(self, context): |
| time.sleep(self._num_seconds) |
| |
| def __repr__(self): |
| return 'Sleep[num_seconds=%s]' % self._num_seconds |
| |
| |
| class RetryAssertAction(Action): |
| """ |
| Action that retries an assertion action a number of times if it fails. |
| |
| An example use case for this action is to verify that a peripheral device |
| appears after power cycling. E.g.: |
| PowerCycleUsbPort(ATRUS), |
| RetryAssertAction(AssertUsbDevices(ATRUS), 10) |
| """ |
| def __init__(self, action, num_tries, retry_delay_seconds=1): |
| """ |
| Initializes. |
| |
| @param action The action to execute. |
| @param num_tries The number of times to try the action before failing |
| for real. Must be more than 0. |
| @param retry_delay_seconds The number of seconds to sleep between |
| retries. |
| |
| @raises ValueError if num_tries is below 1. |
| """ |
| super(RetryAssertAction, self).__init__() |
| if num_tries < 1: |
| raise ValueError('num_tries must be > 0. Was %s' % num_tries) |
| self._action = action |
| self._num_tries = num_tries |
| self._retry_delay_seconds = retry_delay_seconds |
| |
| def do_execute(self, context): |
| for attempt in xrange(self._num_tries): |
| try: |
| self._action.execute(context) |
| return |
| except AssertionError as e: |
| if attempt == self._num_tries - 1: |
| raise e |
| else: |
| logging.info( |
| 'Action %s failed, will retry %d more times', |
| self._action, |
| self._num_tries - attempt - 1, |
| exc_info=True) |
| time.sleep(self._retry_delay_seconds) |
| |
| def __repr__(self): |
| return ('RetryAssertAction[action=%s, ' |
| 'num_tries=%s, retry_delay_seconds=%s]' |
| % (self._action, self._num_tries, self._retry_delay_seconds)) |
| |
| |
| class AssertNoNewCrashes(Action): |
| """ |
| Asserts that no new crash files exist on disk. |
| """ |
| def do_execute(self, context): |
| new_crash_files = context.crash_detector.get_new_crash_files() |
| if new_crash_files: |
| raise AssertionError( |
| 'New crash files detected: %s' % str(new_crash_files)) |
| |
| |
| class TimeoutError(RuntimeError): |
| """ |
| Error raised when an operation times out. |
| """ |
| pass |
| |
| |
| def _wait_for_condition(condition, timeout_seconds=10): |
| """ |
| Wait for a condition to become true. |
| |
| Checks the condition every second. |
| |
| @param condition The condition to check - a function returning a boolean. |
| @param timeout_seconds The timeout in seconds. |
| |
| @raises TimeoutError in case the condition does not become true within |
| the timeout. |
| """ |
| if condition(): |
| return |
| for _ in xrange(timeout_seconds): |
| time.sleep(1) |
| if condition(): |
| return |
| raise TimeoutError('Timeout after %s seconds waiting for condition %s' |
| % (timeout_seconds, condition)) |
| |
| |
| class StartPerfMetricsCollection(Action): |
| """ |
| Starts collecting performance data. |
| |
| Collection is performed in a background thread so this operation returns |
| immediately. |
| |
| This action only collects the data, it does not upload it. |
| Use UploadPerfMetrics to upload the data to the perf dashboard. |
| """ |
| def do_execute(self, context): |
| context.perf_metrics_collector.start() |
| |
| |
| class StopPerfMetricsCollection(Action): |
| """ |
| Stops collecting performance data. |
| |
| This action only stops collecting the data, it does not upload it. |
| Use UploadPerfMetrics to upload the data to the perf dashboard. |
| """ |
| def do_execute(self, context): |
| context.perf_metrics_collector.stop() |
| |
| |
| class UploadPerfMetrics(Action): |
| """ |
| Uploads the collected perf metrics to the perf dashboard. |
| """ |
| def do_execute(self, context): |
| context.perf_metrics_collector.upload_metrics() |
| |
| |
| class CreateMeetingWithBots(Action): |
| """ |
| Creates a new meeting prepopulated with bots. |
| |
| Call JoinMeetingWithBots() do join it with a CfM. |
| """ |
| def __init__(self, bot_count, bots_ttl_min, muted=True): |
| """ |
| Initializes. |
| |
| @param bot_count Amount of bots to be in the meeting. |
| @param bots_ttl_min TTL in minutes after which the bots leave. |
| @param muted If the bots are audio muted or not. |
| """ |
| super(CreateMeetingWithBots, self).__init__() |
| self._bot_count = bot_count |
| # Adds an extra 30 seconds buffer |
| self._bots_ttl_sec = bots_ttl_min * 60 + 30 |
| self._muted = muted |
| |
| def __repr__(self): |
| return ( |
| 'CreateMeetingWithBots:\n' |
| ' bot_count: %d\n' |
| ' bots_ttl_sec: %d\n' |
| ' muted: %s' % (self._bot_count, self._bots_ttl_sec, self._muted) |
| ) |
| |
| def do_execute(self, context): |
| if context.bots_meeting_code: |
| raise AssertionError( |
| 'A meeting with bots is already running. ' |
| 'Repeated calls to CreateMeetingWithBots() are not supported.') |
| context.bots_meeting_code = context.bond_api.CreateConference() |
| context.bond_api.AddBotsRequest( |
| context.bots_meeting_code, |
| self._bot_count, |
| self._bots_ttl_sec); |
| mute_cmd = 'mute_audio' if self._muted else 'unmute_audio' |
| context.bond_api.ExecuteScript('@all %s' % mute_cmd, |
| context.bots_meeting_code) |
| |
| |
| class JoinMeetingWithBots(Action): |
| """ |
| Joins an existing meeting started via CreateMeetingWithBots(). |
| """ |
| def do_execute(self, context): |
| meeting_code = context.bots_meeting_code |
| if not meeting_code: |
| raise AssertionError( |
| 'Meeting with bots was not started. ' |
| 'Did you forget to call CreateMeetingWithBots()?') |
| context.cfm_facade.join_meeting_session(context.bots_meeting_code) |