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