1#!/usr/bin/env python3 2# 3# Copyright 2017 - 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 17from acts import signals 18 19 20def test_info(predicate=None, **keyed_info): 21 """Adds info about test. 22 23 Extra info to include about the test. This info will be available in the 24 test output. Note that if a key is given multiple times it will be added 25 as a list of all values. If multiples of these are stacked their results 26 will be merged. 27 28 Example: 29 # This test will have a variable my_var 30 @test_info(my_var='THIS IS MY TEST') 31 def my_test(self): 32 return False 33 34 Args: 35 predicate: A func to call that if false will skip adding this test 36 info. Function signature is bool(test_obj, args, kwargs) 37 **keyed_info: The key, value info to include in the extras for this 38 test. 39 """ 40 41 def test_info_decorator(func): 42 return _TestInfoDecoratorFunc(func, predicate, keyed_info) 43 44 return test_info_decorator 45 46 47def __select_last(test_signals): 48 return test_signals[-1] 49 50 51def repeated_test(num_passes, acceptable_failures=0, 52 result_selector=__select_last): 53 """A decorator that runs a test case multiple times. 54 55 This decorator can be used to run a test multiple times and aggregate the 56 data into a single test result. By setting `result_selector`, the user can 57 access the returned result of each run, allowing them to average results, 58 return the median, or gather and return standard deviation values. 59 60 This decorator should be used on test cases, and should not be used on 61 static or class methods. 62 63 Note that any TestSignal intended to abort or skip the test will take 64 abort or skip immediately. 65 66 Args: 67 num_passes: The number of times the test needs to pass to report the 68 test case as passing. 69 acceptable_failures: The number of failures accepted. If the failures 70 exceeds this number, the test will stop repeating. The maximum 71 number of runs is `num_passes + acceptable_failures`. If the test 72 does fail, result_selector will still be called. 73 result_selector: A lambda that takes in the list of TestSignals and 74 returns the test signal to report the test case as. Note that the 75 list also contains any uncaught exceptions from the test execution. 76 """ 77 def decorator(func): 78 if not func.__name__.startswith('test_'): 79 raise ValueError('Tests must start with "test_".') 80 81 def test_wrapper(self): 82 num_failures = 0 83 num_seen_passes = 0 84 test_signals_received = [] 85 for i in range(num_passes + acceptable_failures): 86 try: 87 func(self) 88 except (signals.TestFailure, signals.TestError, 89 AssertionError) as signal: 90 test_signals_received.append(signal) 91 num_failures += 1 92 except signals.TestPass as signal: 93 test_signals_received.append(signal) 94 num_seen_passes += 1 95 except (signals.TestSignal, KeyboardInterrupt): 96 raise 97 except Exception as signal: 98 test_signals_received.append(signal) 99 num_failures += 1 100 else: 101 num_seen_passes += 1 102 test_signals_received.append(signals.TestPass( 103 'Test iteration %s of %s passed without details.' % ( 104 i, func.__name__))) 105 106 if num_failures > acceptable_failures: 107 break 108 elif num_seen_passes == num_passes: 109 break 110 else: 111 self.teardown_test() 112 self.setup_test() 113 114 raise result_selector(test_signals_received) 115 116 return test_wrapper 117 118 return decorator 119 120 121def test_tracker_info(uuid, extra_environment_info=None, predicate=None): 122 """Decorator for adding test tracker info to tests results. 123 124 Will add test tracker info inside of Extras/test_tracker_info. 125 126 Example: 127 # This test will be linked to test tracker uuid abcd 128 @test_tracker_info(uuid='abcd') 129 def my_test(self): 130 return False 131 132 Args: 133 uuid: The uuid of the test case in test tracker. 134 extra_environment_info: Extra info about the test tracker environment. 135 predicate: A func that if false when called will ignore this info. 136 """ 137 return test_info( 138 test_tracker_uuid=uuid, 139 test_tracker_environment_info=extra_environment_info, 140 predicate=predicate) 141 142 143class _TestInfoDecoratorFunc(object): 144 """Object that acts as a function decorator test info.""" 145 146 def __init__(self, func, predicate, keyed_info): 147 self.func = func 148 self.predicate = predicate 149 self.keyed_info = keyed_info 150 self.__name__ = func.__name__ 151 self.__doc__ = func.__doc__ 152 self.__module__ = func.__module__ 153 154 def __get__(self, instance, owner): 155 """Called by Python to create a binding for an instance closure. 156 157 When called by Python this object will create a special binding for 158 that instance. That binding will know how to interact with this 159 specific decorator. 160 """ 161 return _TestInfoBinding(self, instance) 162 163 def __call__(self, *args, **kwargs): 164 """ 165 When called runs the underlying func and then attaches test info 166 to a signal. 167 """ 168 cause = None 169 try: 170 result = self.func(*args, **kwargs) 171 172 if result or result is None: 173 new_signal = signals.TestPass('') 174 else: 175 new_signal = signals.TestFailure('') 176 except signals.TestSignal as signal: 177 new_signal = signal 178 except Exception as ex: 179 cause = ex 180 new_signal = signals.TestError(cause) 181 182 if new_signal.extras is None: 183 new_signal.extras = {} 184 if not isinstance(new_signal.extras, dict): 185 raise ValueError('test_info can only append to signal data ' 186 'that has a dict as the extra value.') 187 188 gathered_extras = self._gather_local_info(None, *args, **kwargs) 189 for k, v in gathered_extras.items(): 190 if k not in new_signal.extras: 191 new_signal.extras[k] = v 192 else: 193 if not isinstance(new_signal.extras[k], list): 194 new_signal.extras[k] = [new_signal.extras[k]] 195 196 new_signal.extras[k].insert(0, v) 197 198 raise new_signal from cause 199 200 def gather(self, *args, **kwargs): 201 """ 202 Gathers the info from this decorator without invoking the underlying 203 function. This will also gather all child info if the underlying func 204 has that ability. 205 206 Returns: A dictionary of info. 207 """ 208 if hasattr(self.func, 'gather'): 209 extras = self.func.gather(*args, **kwargs) 210 else: 211 extras = {} 212 213 self._gather_local_info(extras, *args, **kwargs) 214 215 return extras 216 217 def _gather_local_info(self, gather_into, *args, **kwargs): 218 """Gathers info from this decorator and ignores children. 219 220 Args: 221 gather_into: Gathers into a dictionary that already exists. 222 223 Returns: The dictionary with gathered info in it. 224 """ 225 if gather_into is None: 226 extras = {} 227 else: 228 extras = gather_into 229 if not self.predicate or self.predicate(args, kwargs): 230 for k, v in self.keyed_info.items(): 231 if v and k not in extras: 232 extras[k] = v 233 elif v and k in extras: 234 if not isinstance(extras[k], list): 235 extras[k] = [extras[k]] 236 extras[k].insert(0, v) 237 238 return extras 239 240 241class _TestInfoBinding(object): 242 """ 243 When Python creates an instance of an object it creates a binding object 244 for each closure that contains what the instance variable should be when 245 called. This object is a similar binding for _TestInfoDecoratorFunc. 246 When Python tries to create a binding of a _TestInfoDecoratorFunc it 247 will return one of these objects to hold the instance for that closure. 248 """ 249 250 def __init__(self, target, instance): 251 """ 252 Args: 253 target: The target for creating a binding to. 254 instance: The instance to bind the target with. 255 """ 256 self.target = target 257 self.instance = instance 258 self.__name__ = target.__name__ 259 260 def __call__(self, *args, **kwargs): 261 """ 262 When this object is called it will call the target with the bound 263 instance. 264 """ 265 return self.target(self.instance, *args, **kwargs) 266 267 def gather(self, *args, **kwargs): 268 """ 269 Will gather the target with the bound instance. 270 """ 271 return self.target.gather(self.instance, *args, **kwargs) 272