1# Copyright 2018, The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""
16Module Finder class.
17"""
18
19import logging
20import os
21import re
22
23# pylint: disable=import-error
24import atest_error
25import constants
26import test_info
27import test_finder_base
28import test_finder_utils
29from test_runners import atest_tf_test_runner
30from test_runners import robolectric_test_runner
31from test_runners import vts_tf_test_runner
32
33_JAVA_EXT = '.java'
34
35# Parse package name from the package declaration line of a java file.
36# Group matches "foo.bar" of line "package foo.bar;"
37_PACKAGE_RE = re.compile(r'\s*package\s+(?P<package>[^;]+)\s*;\s*', re.I)
38
39_MODULES_IN = 'MODULES-IN-%s'
40_ANDROID_MK = 'Android.mk'
41
42# These are suites in LOCAL_COMPATIBILITY_SUITE that aren't really suites so
43# we can ignore them.
44_SUITES_TO_IGNORE = frozenset({'general-tests', 'device-tests', 'tests'})
45
46
47class ModuleFinder(test_finder_base.TestFinderBase):
48    """Module finder class."""
49    NAME = 'MODULE'
50    _TEST_RUNNER = atest_tf_test_runner.AtestTradefedTestRunner.NAME
51    _ROBOLECTRIC_RUNNER = robolectric_test_runner.RobolectricTestRunner.NAME
52    _VTS_TEST_RUNNER = vts_tf_test_runner.VtsTradefedTestRunner.NAME
53
54    def __init__(self, module_info=None):
55        super(ModuleFinder, self).__init__()
56        self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
57        self.module_info = module_info
58
59    def _has_test_config(self, mod_info):
60        """Validate if this module has a test config.
61
62        A module can have a test config in the following manner:
63          - The module name is not for 2nd architecture.
64          - AndroidTest.xml at the module path.
65          - Auto-generated config via the auto_test_config key in module-info.json.
66
67        Args:
68            mod_info: Dict of module info to check.
69
70        Returns:
71            True if this module has a test config, False otherwise.
72        """
73        # Check if the module is for 2nd architecture.
74        if test_finder_utils.is_2nd_arch_module(mod_info):
75            return False
76
77        # Check for AndroidTest.xml at the module path.
78        for path in mod_info.get(constants.MODULE_PATH, []):
79            if os.path.isfile(os.path.join(self.root_dir, path,
80                                           constants.MODULE_CONFIG)):
81                return True
82
83        # Check if the module has an auto-generated config.
84        return self._is_auto_gen_test_config(mod_info.get(constants.MODULE_NAME))
85
86    def _is_testable_module(self, mod_info):
87        """Check if module is something we can test.
88
89        A module is testable if:
90          - it's installed.
91          - it's a robolectric module (or shares path with one).
92
93        Args:
94            mod_info: Dict of module info to check.
95
96        Returns:
97            True if we can test this module, False otherwise.
98        """
99        if not mod_info:
100            return False
101        if mod_info.get(constants.MODULE_INSTALLED) and self._has_test_config(mod_info):
102            return True
103        if self._is_robolectric_test(mod_info.get(constants.MODULE_NAME)):
104            return True
105        return False
106
107    def _get_first_testable_module(self, path):
108        """Returns first testable module given module path.
109
110        Args:
111            path: String path of module to look for.
112
113        Returns:
114            String of first installed module name.
115        """
116        for mod in self.module_info.get_module_names(path):
117            mod_info = self.module_info.get_module_info(mod)
118            if self._is_testable_module(mod_info):
119                return mod_info.get(constants.MODULE_NAME)
120        return None
121
122    def _is_vts_module(self, module_name):
123        """Returns True if the module is a vts module, else False."""
124        mod_info = self.module_info.get_module_info(module_name)
125        suites = []
126        if mod_info:
127            suites = mod_info.get('compatibility_suites', [])
128        # Pull out all *ts (cts, tvts, etc) suites.
129        suites = [suite for suite in suites if suite not in _SUITES_TO_IGNORE]
130        return len(suites) == 1 and 'vts' in suites
131
132    def _update_to_vts_test_info(self, test):
133        """Fill in the fields with vts specific info.
134
135        We need to update the runner to use the vts runner and also find the
136        test specific depedencies
137
138        Args:
139            test: TestInfo to update with vts specific details.
140
141        Return:
142            TestInfo that is ready for the vts test runner.
143        """
144        test.test_runner = self._VTS_TEST_RUNNER
145        config_file = os.path.join(self.root_dir,
146                                   test.data[constants.TI_REL_CONFIG])
147        # Need to get out dir (special logic is to account for custom out dirs).
148        # The out dir is used to construct the build targets for the test deps.
149        out_dir = os.environ.get(constants.ANDROID_HOST_OUT)
150        custom_out_dir = os.environ.get(constants.ANDROID_OUT_DIR)
151        # If we're not an absolute custom out dir, get relative out dir path.
152        if custom_out_dir is None or not os.path.isabs(custom_out_dir):
153            out_dir = os.path.relpath(out_dir, self.root_dir)
154        vts_out_dir = os.path.join(out_dir, 'vts', 'android-vts', 'testcases')
155
156        # Add in vts test build targets.
157        test.build_targets = test_finder_utils.get_targets_from_vts_xml(
158            config_file, vts_out_dir, self.module_info)
159        test.build_targets.add('vts-test-core')
160        test.build_targets.add(test.test_name)
161        return test
162
163    def _get_robolectric_test_name(self, module_name):
164        """Returns run robolectric module.
165
166        There are at least 2 modules in every robolectric module path, return
167        the module that we can run as a build target.
168
169        Arg:
170            module_name: String of module.
171
172        Returns:
173            String of module that is the run robolectric module, None if none
174            could be found.
175        """
176        module_name_info = self.module_info.get_module_info(module_name)
177        if not module_name_info:
178            return None
179        for mod in self.module_info.get_module_names(
180                module_name_info.get(constants.MODULE_PATH, [])[0]):
181            mod_info = self.module_info.get_module_info(mod)
182            if test_finder_utils.is_robolectric_module(mod_info):
183                return mod
184        return None
185
186    def _is_robolectric_test(self, module_name):
187        """Check if module is a robolectric test.
188
189        A module can be a robolectric test if the specified module has their
190        class set as ROBOLECTRIC (or shares their path with a module that does).
191
192        Args:
193            module_name: String of module to check.
194
195        Returns:
196            True if the module is a robolectric module, else False.
197        """
198        # Check 1, module class is ROBOLECTRIC
199        mod_info = self.module_info.get_module_info(module_name)
200        if mod_info and test_finder_utils.is_robolectric_module(mod_info):
201            return True
202        # Check 2, shared modules in the path have class ROBOLECTRIC_CLASS.
203        if self._get_robolectric_test_name(module_name):
204            return True
205        return False
206
207    def _update_to_robolectric_test_info(self, test):
208        """Update the fields for a robolectric test.
209
210        Args:
211          test: TestInfo to be updated with robolectric fields.
212
213        Returns:
214          TestInfo with robolectric fields.
215        """
216        test.test_runner = self._ROBOLECTRIC_RUNNER
217        test.test_name = self._get_robolectric_test_name(test.test_name)
218        return test
219
220    def _process_test_info(self, test):
221        """Process the test info and return some fields updated/changed.
222
223        We need to check if the test found is a special module (like vts) and
224        update the test_info fields (like test_runner) appropriately.
225
226        Args:
227            test: TestInfo that has been filled out by a find method.
228
229        Return:
230            TestInfo that has been modified as needed.
231        """
232        # Check if this is only a vts module.
233        if self._is_vts_module(test.test_name):
234            return self._update_to_vts_test_info(test)
235        elif self._is_robolectric_test(test.test_name):
236            return self._update_to_robolectric_test_info(test)
237        module_name = test.test_name
238        rel_config = test.data[constants.TI_REL_CONFIG]
239        test.build_targets = self._get_build_targets(module_name, rel_config)
240        return test
241
242    def _is_auto_gen_test_config(self, module_name):
243        """Check if the test config file will be generated automatically.
244
245        Args:
246            module_name: A string of the module name.
247
248        Returns:
249            True if the test config file will be generated automatically.
250        """
251        if self.module_info.is_module(module_name):
252            mod_info = self.module_info.get_module_info(module_name)
253            auto_test_config = mod_info.get('auto_test_config', [])
254            return auto_test_config and auto_test_config[0]
255        return False
256
257    def _get_build_targets(self, module_name, rel_config):
258        """Get the test deps.
259
260        Args:
261            module_name: name of the test.
262            rel_config: XML for the given test.
263
264        Returns:
265            Set of build targets.
266        """
267        targets = set()
268        if not self._is_auto_gen_test_config(module_name):
269            config_file = os.path.join(self.root_dir, rel_config)
270            targets = test_finder_utils.get_targets_from_xml(config_file,
271                                                             self.module_info)
272        mod_dir = os.path.dirname(rel_config).replace('/', '-')
273        targets.add(_MODULES_IN % mod_dir)
274        return targets
275
276    def find_test_by_module_name(self, module_name):
277        """Find test for the given module name.
278
279        Args:
280            module_name: A string of the test's module name.
281
282        Returns:
283            A populated TestInfo namedtuple if found, else None.
284        """
285        mod_info = self.module_info.get_module_info(module_name)
286        if self._is_testable_module(mod_info):
287            # path is a list with only 1 element.
288            rel_config = os.path.join(mod_info['path'][0],
289                                      constants.MODULE_CONFIG)
290            return self._process_test_info(test_info.TestInfo(
291                test_name=module_name,
292                test_runner=self._TEST_RUNNER,
293                build_targets=set(),
294                data={constants.TI_REL_CONFIG: rel_config,
295                      constants.TI_FILTER: frozenset()}))
296        return None
297
298    def find_test_by_class_name(self, class_name, module_name=None,
299                                rel_config=None):
300        """Find test files given a class name.
301
302        If module_name and rel_config not given it will calculate it determine
303        it by looking up the tree from the class file.
304
305        Args:
306            class_name: A string of the test's class name.
307            module_name: Optional. A string of the module name to use.
308            rel_config: Optional. A string of module dir relative to repo root.
309
310        Returns:
311            A populated TestInfo namedtuple if test found, else None.
312        """
313        class_name, methods = test_finder_utils.split_methods(class_name)
314        if rel_config:
315            search_dir = os.path.join(self.root_dir,
316                                      os.path.dirname(rel_config))
317        else:
318            search_dir = self.root_dir
319        test_path = test_finder_utils.find_class_file(search_dir, class_name)
320        if not test_path and rel_config:
321            logging.info('Did not find class (%s) under module path (%s), '
322                         'researching from repo root.', class_name, rel_config)
323            test_path = test_finder_utils.find_class_file(self.root_dir,
324                                                          class_name)
325        if not test_path:
326            return None
327        full_class_name = test_finder_utils.get_fully_qualified_class_name(
328            test_path)
329        test_filter = frozenset([test_info.TestFilter(full_class_name,
330                                                      methods)])
331        if not rel_config:
332            test_dir = os.path.dirname(test_path)
333            rel_module_dir = test_finder_utils.find_parent_module_dir(
334                self.root_dir, test_dir, self.module_info)
335            rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG)
336        if not module_name:
337            module_name = self._get_first_testable_module(os.path.dirname(
338                rel_config))
339        return self._process_test_info(test_info.TestInfo(
340            test_name=module_name,
341            test_runner=self._TEST_RUNNER,
342            build_targets=set(),
343            data={constants.TI_FILTER: test_filter,
344                  constants.TI_REL_CONFIG: rel_config}))
345
346    def find_test_by_module_and_class(self, module_class):
347        """Find the test info given a MODULE:CLASS string.
348
349        Args:
350            module_class: A string of form MODULE:CLASS or MODULE:CLASS#METHOD.
351
352        Returns:
353            A populated TestInfo namedtuple if found, else None.
354        """
355        if ':' not in module_class:
356            return None
357        module_name, class_name = module_class.split(':')
358        module_info = self.find_test_by_module_name(module_name)
359        if not module_info:
360            return None
361        return self.find_test_by_class_name(
362            class_name, module_info.test_name,
363            module_info.data.get(constants.TI_REL_CONFIG))
364
365    def find_test_by_package_name(self, package, module_name=None,
366                                  rel_config=None):
367        """Find the test info given a PACKAGE string.
368
369        Args:
370            package: A string of the package name.
371            module_name: Optional. A string of the module name.
372            ref_config: Optional. A string of rel path of config.
373
374        Returns:
375            A populated TestInfo namedtuple if found, else None.
376        """
377        _, methods = test_finder_utils.split_methods(package)
378        if methods:
379            raise atest_error.MethodWithoutClassError('Method filtering '
380                                                      'requires class')
381        # Confirm that packages exists and get user input for multiples.
382        if rel_config:
383            search_dir = os.path.join(self.root_dir,
384                                      os.path.dirname(rel_config))
385        else:
386            search_dir = self.root_dir
387        package_path = test_finder_utils.run_find_cmd(
388            test_finder_utils.FIND_REFERENCE_TYPE.PACKAGE, search_dir,
389            package.replace('.', '/'))
390        # package path will be the full path to the dir represented by package
391        if not package_path:
392            return None
393        test_filter = frozenset([test_info.TestFilter(package, frozenset())])
394        if not rel_config:
395            rel_module_dir = test_finder_utils.find_parent_module_dir(
396                self.root_dir, package_path, self.module_info)
397            rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG)
398        if not module_name:
399            module_name = self._get_first_testable_module(
400                os.path.dirname(rel_config))
401        return self._process_test_info(test_info.TestInfo(
402            test_name=module_name,
403            test_runner=self._TEST_RUNNER,
404            build_targets=set(),
405            data={constants.TI_FILTER: test_filter,
406                  constants.TI_REL_CONFIG: rel_config}))
407
408    def find_test_by_module_and_package(self, module_package):
409        """Find the test info given a MODULE:PACKAGE string.
410
411        Args:
412            module_package: A string of form MODULE:PACKAGE
413
414        Returns:
415            A populated TestInfo namedtuple if found, else None.
416        """
417        module_name, package = module_package.split(':')
418        module_info = self.find_test_by_module_name(module_name)
419        if not module_info:
420            return None
421        return self.find_test_by_package_name(
422            package, module_info.test_name,
423            module_info.data.get(constants.TI_REL_CONFIG))
424
425    def find_test_by_path(self, path):
426        """Find the first test info matching the given path.
427
428        Strategy:
429            path_to_java_file --> Resolve to CLASS
430            path_to_module_file -> Resolve to MODULE
431            path_to_module_dir -> Resolve to MODULE
432            path_to_dir_with_class_files--> Resolve to PACKAGE
433            path_to_any_other_dir --> Resolve as MODULE
434
435        Args:
436            path: A string of the test's path.
437
438        Returns:
439            A populated TestInfo namedtuple if test found, else None
440        """
441        logging.debug('Finding test by path: %s', path)
442        path, methods = test_finder_utils.split_methods(path)
443        # TODO: See if this can be generalized and shared with methods above
444        # create absolute path from cwd and remove symbolic links
445        path = os.path.realpath(path)
446        if not os.path.exists(path):
447            return None
448        dir_path, file_name = test_finder_utils.get_dir_path_and_filename(path)
449        # Module/Class
450        rel_module_dir = test_finder_utils.find_parent_module_dir(
451            self.root_dir, dir_path, self.module_info)
452        if not rel_module_dir:
453            return None
454        module_name = self._get_first_testable_module(rel_module_dir)
455        rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG)
456        data = {constants.TI_REL_CONFIG: rel_config,
457                constants.TI_FILTER: frozenset()}
458        # Path is to java file
459        if file_name and file_name.endswith(_JAVA_EXT):
460            full_class_name = test_finder_utils.get_fully_qualified_class_name(
461                path)
462            data[constants.TI_FILTER] = frozenset(
463                [test_info.TestFilter(full_class_name, methods)])
464        # path to non-module dir, treat as package
465        elif (not file_name and not self._is_auto_gen_test_config(module_name)
466              and rel_module_dir != os.path.relpath(path, self.root_dir)):
467            dir_items = [os.path.join(path, f) for f in os.listdir(path)]
468            for dir_item in dir_items:
469                if dir_item.endswith(_JAVA_EXT):
470                    package_name = test_finder_utils.get_package_name(dir_item)
471                    if package_name:
472                        # methods should be empty frozenset for package.
473                        if methods:
474                            raise atest_error.MethodWithoutClassError()
475                        data[constants.TI_FILTER] = frozenset(
476                            [test_info.TestFilter(package_name, methods)])
477                        break
478        return self._process_test_info(test_info.TestInfo(
479            test_name=module_name,
480            test_runner=self._TEST_RUNNER,
481            build_targets=set(),
482            data=data))
483