• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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_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