1#!/usr/bin/env python3
2#
3#   Copyright 2018 - 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.
16from __future__ import absolute_import
17
18import os
19import sys
20
21# A temporary hack to prevent tests/libs/logging from being selected as the
22# python default logging module.
23sys.path[0] = os.path.join(sys.path[0], '../')
24import mock
25import shutil
26import tempfile
27import unittest
28
29from acts import base_test
30from acts.libs import version_selector
31from acts.test_decorators import test_tracker_info
32
33from mobly.config_parser import TestRunConfig
34
35
36def versioning_decorator(min_sdk, max_sdk):
37    return version_selector.set_version(lambda ret, *_, **__: ret, min_sdk,
38                                        max_sdk)
39
40
41def versioning_decorator2(min_sdk, max_sdk):
42    return version_selector.set_version(lambda ret, *_, **__: ret, min_sdk,
43                                        max_sdk)
44
45
46def test_versioning(min_sdk, max_sdk):
47    return version_selector.set_version(lambda *_, **__: 1, min_sdk, max_sdk)
48
49
50@versioning_decorator(1, 10)
51def versioned_func(arg1, arg2):
52    return 'function 1', arg1, arg2
53
54
55@versioning_decorator(11, 11)
56def versioned_func(arg1, arg2):
57    return 'function 2', arg1, arg2
58
59
60@versioning_decorator(12, 20)
61def versioned_func(arg1, arg2):
62    return 'function 3', arg1, arg2
63
64
65@versioning_decorator(1, 20)
66def versioned_func_with_kwargs(_, asdf='jkl'):
67    return asdf
68
69
70def class_versioning_decorator(min_sdk, max_sdk):
71    return version_selector.set_version(lambda _, ret, *__, **___: ret,
72                                        min_sdk, max_sdk)
73
74
75class VersionedClass(object):
76    @classmethod
77    @class_versioning_decorator(1, 99999999)
78    def class_func(cls, arg1):
79        return cls, arg1
80
81    @staticmethod
82    @versioning_decorator(1, 99999999)
83    def static_func(arg1):
84        return arg1
85
86    @class_versioning_decorator(1, 99999999)
87    def instance_func(self, arg1):
88        return self, arg1
89
90
91class VersionedTestClass(base_test.BaseTestClass):
92    @mock.patch('mobly.utils.create_dir')
93    def __init__(self, configs, _):
94        super().__init__(configs)
95
96    @test_tracker_info('UUID_1')
97    @test_versioning(1, 1)
98    def test_1(self):
99        pass
100
101    @test_versioning(1, 1)
102    @test_tracker_info('UUID_2')
103    def test_2(self):
104        pass
105
106
107class VersionSelectorIntegrationTest(unittest.TestCase):
108    """Tests the acts.libs.version_selector module."""
109
110    @classmethod
111    def setUpClass(cls):
112        cls.tmp_dir = tempfile.mkdtemp()
113
114    @classmethod
115    def tearDownClass(cls):
116        shutil.rmtree(cls.tmp_dir)
117
118    def test_versioned_test_class_calls_both_functions(self):
119        """Tests that VersionedTestClass (above) can be called with
120        test_tracker_info."""
121        test_run_config = TestRunConfig()
122        test_run_config.log_path = ''
123        test_run_config.summary_writer = mock.MagicMock()
124        test_run_config.log_path = self.tmp_dir
125
126        test_class = VersionedTestClass(test_run_config)
127        test_class.run(['test_1', 'test_2'])
128
129        self.assertIn('Executed 2', test_class.results.summary_str(),
130                      'One or more of the test cases did not execute.')
131        self.assertEqual(
132            'UUID_1',
133            test_class.results.executed[0].extras['test_tracker_uuid'],
134            'The test_tracker_uuid was not found for test_1.')
135        self.assertEqual(
136            'UUID_2',
137            test_class.results.executed[1].extras['test_tracker_uuid'],
138            'The test_tracker_uuid was not found for test_2.')
139
140    def test_raises_syntax_error_if_decorated_with_staticmethod_first(self):
141        try:
142
143            class SomeClass(object):
144                @versioning_decorator(1, 1)
145                @staticmethod
146                def test_1():
147                    pass
148        except SyntaxError:
149            pass
150        else:
151            self.fail('Placing the @staticmethod decorator after the '
152                      'versioning decorator should cause a SyntaxError.')
153
154    def test_raises_syntax_error_if_decorated_with_classmethod_first(self):
155        try:
156
157            class SomeClass(object):
158                @versioning_decorator(1, 1)
159                @classmethod
160                def test_1(cls):
161                    pass
162        except SyntaxError:
163            pass
164        else:
165            self.fail('Placing the @classmethod decorator after the '
166                      'versioning decorator should cause a SyntaxError.')
167
168    def test_overriding_an_undecorated_func_raises_a_syntax_error(self):
169        try:
170
171            class SomeClass(object):
172                def test_1(self):
173                    pass
174
175                @versioning_decorator(1, 1)
176                def test_1(self):
177                    pass
178        except SyntaxError:
179            pass
180        else:
181            self.fail('Overwriting a function that already exists without a '
182                      'versioning decorator should raise a SyntaxError.')
183
184    def test_func_decorated_with_2_different_versioning_decorators_causes_error(
185            self):
186        try:
187
188            class SomeClass(object):
189                @versioning_decorator(1, 1)
190                def test_1(self):
191                    pass
192
193                @versioning_decorator2(2, 2)
194                def test_1(self):
195                    pass
196        except SyntaxError:
197            pass
198        else:
199            self.fail('Using two different versioning decorators to version a '
200                      'single function should raise a SyntaxError.')
201
202    def test_func_decorated_with_overlapping_ranges_causes_value_error(self):
203        try:
204
205            class SomeClass(object):
206                @versioning_decorator(1, 2)
207                def test_1(self):
208                    pass
209
210                @versioning_decorator(2, 2)
211                def test_1(self):
212                    pass
213        except ValueError:
214            pass
215        else:
216            self.fail('Decorated functions with overlapping version ranges '
217                      'should raise a ValueError.')
218
219    def test_func_decorated_with_min_gt_max_causes_value_error(self):
220        try:
221
222            class SomeClass(object):
223                @versioning_decorator(2, 1)
224                def test_1(self):
225                    pass
226        except ValueError:
227            pass
228        else:
229            self.fail(
230                'If the min_version level is higher than the max_version '
231                'level, a ValueError should be raised.')
232
233    def test_calling_versioned_func_on_min_version_level_is_inclusive(self):
234        """Tests that calling some versioned function with the minimum version
235        level of the decorated function will call that function."""
236        ret = versioned_func(1, 'some_value')
237        self.assertEqual(
238            ret, ('function 1', 1, 'some_value'),
239            'Calling versioned_func(1, ...) did not return the '
240            'versioned function for the correct range.')
241
242    def test_calling_versioned_func_on_middle_level_works(self):
243        """Tests that calling some versioned function a version value within the
244        range of the decorated function will call that function."""
245        ret = versioned_func(16, 'some_value')
246        self.assertEqual(
247            ret, ('function 3', 16, 'some_value'),
248            'Calling versioned_func(16, ...) did not return the '
249            'versioned function for the correct range.')
250
251    def test_calling_versioned_func_on_max_version_level_is_inclusive(self):
252        """Tests that calling some versioned function with the maximum version
253        level of the decorated function will call that function."""
254        ret = versioned_func(10, 'some_value')
255        self.assertEqual(
256            ret, ('function 1', 10, 'some_value'),
257            'Calling versioned_func(10, ...) did not return the '
258            'versioned function for the correct range.')
259
260    def test_calling_versioned_func_on_min_equals_max_level_works(self):
261        """Tests that calling some versioned function with the maximum version
262        level of the decorated function will call that function."""
263        ret = versioned_func(11, 'some_value')
264        self.assertEqual(
265            ret, ('function 2', 11, 'some_value'),
266            'Calling versioned_func(10, ...) did not return the '
267            'versioned function for the correct range.')
268
269    def test_sending_kwargs_through_decorated_functions_works(self):
270        """Tests that calling some versioned function with the maximum version
271        level of the decorated function will call that function."""
272        ret = versioned_func_with_kwargs(1, asdf='some_value')
273        self.assertEqual(
274            ret, 'some_value',
275            'Calling versioned_func_with_kwargs(1, ...) did not'
276            'return the kwarg value properly.')
277
278    def test_kwargs_can_default_through_decorated_functions(self):
279        """Tests that calling some versioned function with the maximum version
280        level of the decorated function will call that function."""
281        ret = versioned_func_with_kwargs(1)
282        self.assertEqual(
283            ret, 'jkl', 'Calling versioned_func_with_kwargs(1) did not'
284            'return the default kwarg value properly.')
285
286    def test_staticmethod_can_be_called_properly(self):
287        """Tests that decorating a staticmethod will properly send the arguments
288        in the correct order.
289
290        i.e., we want to make sure self or cls do not get sent as the first
291        argument to the decorated staticmethod.
292        """
293        versioned_class = VersionedClass()
294        ret = versioned_class.static_func(123456)
295        self.assertEqual(
296            ret, 123456, 'The first argument was not set properly for calling '
297            'a staticmethod.')
298
299    def test_instance_method_can_be_called_properly(self):
300        """Tests that decorating a method will properly send the arguments
301        in the correct order.
302
303        i.e., we want to make sure self is the first argument returned.
304        """
305        versioned_class = VersionedClass()
306        ret = versioned_class.instance_func(123456)
307        self.assertEqual(
308            ret, (versioned_class, 123456),
309            'The arguments were not set properly for an instance '
310            'method.')
311
312    def test_classmethod_can_be_called_properly(self):
313        """Tests that decorating a classmethod will properly send the arguments
314        in the correct order.
315
316        i.e., we want to make sure cls is the first argument returned.
317        """
318        versioned_class = VersionedClass()
319        ret = versioned_class.class_func(123456)
320        self.assertEqual(
321            ret, (VersionedClass, 123456),
322            'The arguments were not set properly for a '
323            'classmethod.')
324
325
326if __name__ == '__main__':
327    unittest.main()
328