You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

505 lines
17 KiB

"""
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)