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 signals.TestSignal as signal:
99            new_signal = signal
100        except Exception as cause:
101            new_signal = signals.TestError(cause)
102
103        if new_signal.extras is None:
104            new_signal.extras = {}
105        if not isinstance(new_signal.extras, dict):
106            raise ValueError('test_info can only append to signal data '
107                             'that has a dict as the extra value.')
108
109        gathered_extras = self._gather_local_info(None, *args, **kwargs)
110        for k, v in gathered_extras.items():
111            if k not in new_signal.extras:
112                new_signal.extras[k] = v
113            else:
114                if not isinstance(new_signal.extras[k], list):
115                    new_signal.extras[k] = [new_signal.extras[k]]
116
117                new_signal.extras[k].insert(0, v)
118
119        raise new_signal
120
121    def gather(self, *args, **kwargs):
122        """
123        Gathers the info from this decorator without invoking the underlying
124        function. This will also gather all child info if the underlying func
125        has that ability.
126
127        Returns: A dictionary of info.
128        """
129        if hasattr(self.func, 'gather'):
130            extras = self.func.gather(*args, **kwargs)
131        else:
132            extras = {}
133
134        self._gather_local_info(extras, *args, **kwargs)
135
136        return extras
137
138    def _gather_local_info(self, gather_into, *args, **kwargs):
139        """Gathers info from this decorator and ignores children.
140
141        Args:
142            gather_into: Gathers into a dictionary that already exists.
143
144        Returns: The dictionary with gathered info in it.
145        """
146        if gather_into is None:
147            extras = {}
148        else:
149            extras = gather_into
150        if not self.predicate or self.predicate(args, kwargs):
151            for k, v in self.keyed_info.items():
152                if v and k not in extras:
153                    extras[k] = v
154                elif v and k in extras:
155                    if not isinstance(extras[k], list):
156                        extras[k] = [extras[k]]
157                    extras[k].insert(0, v)
158
159        return extras
160
161
162class _TestInfoBinding(object):
163    """
164    When Python creates an instance of an object it creates a binding object
165    for each closure that contains what the instance variable should be when
166    called. This object is a similar binding for _TestInfoDecoratorFunc.
167    When Python tries to create a binding of a _TestInfoDecoratorFunc it
168    will return one of these objects to hold the instance for that closure.
169    """
170
171    def __init__(self, target, instance):
172        """
173        Args:
174            target: The target for creating a binding to.
175            instance: The instance to bind the target with.
176        """
177        self.target = target
178        self.instance = instance
179        self.__name__ = target.__name__
180
181    def __call__(self, *args, **kwargs):
182        """
183        When this object is called it will call the target with the bound
184        instance.
185        """
186        return self.target(self.instance, *args, **kwargs)
187
188    def gather(self, *args, **kwargs):
189        """
190        Will gather the target with the bound instance.
191        """
192        return self.target.gather(self.instance, *args, **kwargs)
193