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.controllers.buds_lib import tako_trace_logger 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.log_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 = tako_trace_logger.TakoTraceLogger() 139 self.log_step = self.logger.step 140 else: 141 self.logger = logger 142 self.log_step = self.logger.info 143 self.timer = TimeRecorder() 144 self._fill_default_action_map() 145 146 def __enter__(self): 147 return self 148 149 def __exit__(self, *args): 150 pass 151 152 def _perform_action(self, action_name, *args, **kwargs): 153 """Perform the specified human readable action.""" 154 if action_name not in self._action_map: 155 raise TestActionNotFoundError('Action %s not found this class.' 156 % action_name) 157 158 method = self._action_map[action_name] 159 ret = getattr(self, method)(*args, **kwargs) 160 return ret 161 162 @timed_action 163 def print_actions(self): 164 """Example action methods. 165 166 All test action method must: 167 1. return a value. False means action failed, any other value means 168 pass. 169 2. should not start with "_". Methods start with "_" is hidden. 170 All test action method may: 171 1. have optional arguments. Mutable argument can be used to pass 172 value 173 2. raise exceptions. Test case class is expected to handle 174 exceptions 175 """ 176 num_acts = len(self._action_map) 177 178 self.logger.info('I can do %d action%s:' % 179 (num_acts, 's' if num_acts != 1 else '')) 180 for act in self._action_map.keys(): 181 self.logger.info(' - %s' % act) 182 return True 183 184 @timed_action 185 def sleep(self, seconds): 186 self.logger.info('%s seconds' % seconds) 187 time.sleep(seconds) 188 189 190if __name__ == '__main__': 191 acts = BaseTestAction() 192 acts.print_actions() 193 acts._perform_action('print actions') 194 print(acts._get_action_names()) 195