1#!/usr/bin/env python3
2#
3#   Copyright 2018 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17"""Base test action class, provide a base class for representing a collection of
18test actions.
19"""
20
21import datetime
22import inspect
23import time
24
25from acts import tracelogger
26from acts.libs.utils.timer import TimeRecorder
27
28# All methods start with "_" are considered hidden.
29DEFAULT_HIDDEN_ACTION_PREFIX = '_'
30
31
32def timed_action(method):
33    """A common decorator for test actions."""
34
35    def timed(self, *args, **kw):
36        """Log the enter/exit/time of the action method."""
37        func_name = self._convert_default_action_name(method.__name__)
38        if not func_name:
39            func_name = method.__name__
40        self.logger.step('%s...' % func_name)
41        self.timer.start_timer(func_name, True)
42        result = method(self, *args, **kw)
43        # TODO: Method run time collected can be used for automatic KPI checks
44        self.timer.stop_timer(func_name)
45        return result
46
47    return timed
48
49
50class TestActionNotFoundError(Exception):
51    pass
52
53
54class BaseTestAction(object):
55    """Class for organizing a collection of test actions.
56
57    Test actions are just normal python methods, and should perform a specified
58    action. @timed_action decorator can log the entry/exit of the test action,
59    and the execution time.
60
61    The BaseTestAction class also provides a mapping between human friendly
62    names and test action methods in order to support configuration base
63    execution. By default, all methods not hidden (not start with "_") is
64    exported as human friendly name by replacing "_" with space.
65
66    Test action method can be called directly, or via
67    _perform_action(<human friendly name>, <args...>)
68    method.
69    """
70
71    @classmethod
72    def _fill_default_action_map(cls):
73        """Parse current class and get all test actions methods."""
74        # a <human readable name>:<method name> map.
75        cls._action_map = dict()
76        for name, _ in inspect.getmembers(cls, inspect.ismethod):
77            act_name = cls._convert_default_action_name(name)
78            if act_name:
79                cls._action_map[act_name] = name
80
81    @classmethod
82    def _convert_default_action_name(cls, func_name):
83        """Default conversion between method name -> human readable action name.
84        """
85        if not func_name.startswith(DEFAULT_HIDDEN_ACTION_PREFIX):
86            act_name = func_name.lower()
87            act_name = act_name.replace('_', ' ')
88            act_name = act_name.title()
89            return act_name.strip()
90        else:
91            return ''
92
93    @classmethod
94    def _add_action_alias(cls, default_act_name, alias):
95        """Add an alias to an existing test action."""
96        if default_act_name in cls._action_map:
97            cls._action_map[alias] = cls._action_map[default_act_name]
98            return True
99        else:
100            return False
101
102    @classmethod
103    def _get_action_names(cls):
104        if not hasattr(cls, '_action_map'):
105            cls._fill_default_action_map()
106        return cls._action_map.keys()
107
108    @classmethod
109    def get_current_time_logcat_format(cls):
110        return datetime.datetime.now().strftime('%m-%d %H:%M:%S.000')
111
112    @classmethod
113    def _action_exists(cls, action_name):
114        """Verify if an human friendly action name exists or not."""
115        if not hasattr(cls, '_action_map'):
116            cls._fill_default_action_map()
117        return action_name in cls._action_map
118
119    @classmethod
120    def _validate_actions(cls, action_list):
121        """Verify if an human friendly action name exists or not.
122
123        Args:
124          :param action_list: list of actions to be validated.
125
126        Returns:
127          tuple of (is valid, list of invalid/non-existent actions)
128        """
129        not_found = []
130        for action_name in action_list:
131            if not cls._action_exists(action_name):
132                not_found.append(action_name)
133        all_valid = False if not_found else True
134        return all_valid, not_found
135
136    def __init__(self, logger=None):
137        if logger is None:
138            self.logger = tracelogger.TakoTraceLogger()
139        else:
140            self.logger = logger
141        self.timer = TimeRecorder()
142        self._fill_default_action_map()
143
144    def __enter__(self):
145        return self
146
147    def __exit__(self, *args):
148        pass
149
150    def _perform_action(self, action_name, *args, **kwargs):
151        """Perform the specified human readable action."""
152        if action_name not in self._action_map:
153            raise TestActionNotFoundError('Action %s not found this class.'
154                                          % action_name)
155
156        method = self._action_map[action_name]
157        ret = getattr(self, method)(*args, **kwargs)
158        return ret
159
160    @timed_action
161    def print_actions(self):
162        """Example action methods.
163
164        All test action method must:
165            1. return a value. False means action failed, any other value means
166               pass.
167            2. should not start with "_". Methods start with "_" is hidden.
168        All test action method may:
169            1. have optional arguments. Mutable argument can be used to pass
170               value
171            2. raise exceptions. Test case class is expected to handle
172               exceptions
173        """
174        num_acts = len(self._action_map)
175        self.logger.i('I can do %d action%s:' %
176                      (num_acts, 's' if num_acts != 1 else ''))
177        for act in self._action_map.keys():
178            self.logger.i(' - %s' % act)
179        return True
180
181    @timed_action
182    def sleep(self, seconds):
183        self.logger.i('%s seconds' % seconds)
184        time.sleep(seconds)
185
186
187if __name__ == '__main__':
188    acts = BaseTestAction()
189    acts.print_actions()
190    acts._perform_action('print actions')
191    print(acts._get_action_names())
192