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