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 19def test_info(predicate=None, **keyed_info): 20 """Adds info about test. 21 22 Extra Info to include about the test. This info will be available in the 23 test output. Note that if a key is given multiple times it will be added 24 as a list of all values. If multiples of these are stacked there results 25 will be merged. 26 27 Example: 28 # This test will have a variable my_var 29 @test_info(my_var='THIS IS MY TEST') 30 def my_test(self): 31 return False 32 33 Args: 34 predicate: A func to call that if false will skip adding this test 35 info. Function signature is bool(test_obj, args, kwargs) 36 **keyed_info: The key, value info to include in the extras for this 37 test. 38 """ 39 40 def test_info_decoractor(func): 41 return _TestInfoDecoratorFunc(func, predicate, keyed_info) 42 43 return test_info_decoractor 44 45 46def test_tracker_info(uuid, extra_environment_info=None, predicate=None): 47 """Decorator for adding test tracker info to tests results. 48 49 Will add test tracker info inside of Extras/test_tracker_info. 50 51 Example: 52 # This test will be linked to test tracker uuid abcd 53 @test_tracker_info(uuid='abcd') 54 def my_test(self): 55 return False 56 57 Args: 58 uuid: The uuid of the test case in test tracker. 59 extra_environment_info: Extra info about the test tracker environment. 60 predicate: A func that if false when called will ignore this info. 61 """ 62 return test_info( 63 test_tracker_uuid=uuid, 64 test_tracker_enviroment_info=extra_environment_info, 65 predicate=predicate) 66 67 68class _TestInfoDecoratorFunc(object): 69 """Object that acts as a function decorator test info.""" 70 71 def __init__(self, func, predicate, keyed_info): 72 self.func = func 73 self.predicate = predicate 74 self.keyed_info = keyed_info 75 self.__name__ = func.__name__ 76 77 def __get__(self, instance, owner): 78 """Called by Python to create a binding for an instance closure. 79 80 When called by Python this object will create a special binding for 81 that instance. That binding will know how to interact with this 82 specific decorator. 83 """ 84 return _TestInfoBinding(self, instance) 85 86 def __call__(self, *args, **kwargs): 87 """ 88 When called runs the underlying func and then attaches test info 89 to a signal. 90 """ 91 try: 92 result = self.func(*args, **kwargs) 93 94 if result or result is None: 95 new_signal = signals.TestPass('') 96 else: 97 new_signal = signals.TestFailure('') 98 except Exception as signal: 99 new_signal = signal 100 101 if getattr(new_signal, "extras", None) is None: 102 setattr(new_signal, "extras", {}) 103 if not isinstance(new_signal.extras, dict): 104 raise ValueError('test_info can only append to signal data ' 105 'that has a dict as the extra value.') 106 107 gathered_extras = self._gather_local_info(None, *args, **kwargs) 108 for k, v in gathered_extras.items(): 109 if k not in new_signal.extras: 110 new_signal.extras[k] = v 111 else: 112 if not isinstance(new_signal.extras[k], list): 113 new_signal.extras[k] = [new_signal.extras[k]] 114 115 new_signal.extras[k].insert(0, v) 116 117 raise new_signal 118 119 def gather(self, *args, **kwargs): 120 """ 121 Gathers the info from this decorator without invoking the underlying 122 function. This will also gather all child info if the underlying func 123 has that ability. 124 125 Returns: A dictionary of info. 126 """ 127 if hasattr(self.func, 'gather'): 128 extras = self.func.gather(*args, **kwargs) 129 else: 130 extras = {} 131 132 self._gather_local_info(extras, *args, **kwargs) 133 134 return extras 135 136 def _gather_local_info(self, gather_into, *args, **kwargs): 137 """Gathers info from this decorator and ignores children. 138 139 Args: 140 gather_into: Gathers into a dictionary that already exists. 141 142 Returns: The dictionary with gathered info in it. 143 """ 144 if gather_into is None: 145 extras = {} 146 else: 147 extras = gather_into 148 if not self.predicate or self.predicate(args, kwargs): 149 for k, v in self.keyed_info.items(): 150 if v and k not in extras: 151 extras[k] = v 152 elif v and k in extras: 153 if not isinstance(extras[k], list): 154 extras[k] = [extras[k]] 155 extras[k].insert(0, v) 156 157 return extras 158 159 160class _TestInfoBinding(object): 161 """ 162 When Python creates an instance of an object it creates a binding object 163 for each closure that contains what the instance variable should be when 164 called. This object is a similar binding for _TestInfoDecoratorFunc. 165 When Python tries to create a binding of a _TestInfoDecoratorFunc it 166 will return one of these objects to hold the instance for that closure. 167 """ 168 169 def __init__(self, target, instance): 170 """ 171 Args: 172 target: The target for creating a binding to. 173 instance: The instance to bind the target with. 174 """ 175 self.target = target 176 self.instance = instance 177 self.__name__ = target.__name__ 178 179 def __call__(self, *args, **kwargs): 180 """ 181 When this object is called it will call the target with the bound 182 instance. 183 """ 184 return self.target(self.instance, *args, **kwargs) 185 186 def gather(self, *args, **kwargs): 187 """ 188 Will gather the target with the bound instance. 189 """ 190 return self.target.gather(self.instance, *args, **kwargs) 191