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