• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
21
22# pylint: disable=import-error
23import atest_error
24import atest_utils
25import constants
26from test_finders import test_info
27from test_finders import test_finder_base
28from test_finders import 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_MODULES_IN = 'MODULES-IN-%s'
34_ANDROID_MK = 'Android.mk'
35
36# These are suites in LOCAL_COMPATIBILITY_SUITE that aren't really suites so
37# we can ignore them.
38_SUITES_TO_IGNORE = frozenset({'general-tests', 'device-tests', 'tests'})
39
40class ModuleFinder(test_finder_base.TestFinderBase):
41    """Module finder class."""
42    NAME = 'MODULE'
43    _TEST_RUNNER = atest_tf_test_runner.AtestTradefedTestRunner.NAME
44    _ROBOLECTRIC_RUNNER = robolectric_test_runner.RobolectricTestRunner.NAME
45    _VTS_TEST_RUNNER = vts_tf_test_runner.VtsTradefedTestRunner.NAME
46
47    def __init__(self, module_info=None):
48        super(ModuleFinder, self).__init__()
49        self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
50        self.module_info = module_info
51
52    def _determine_testable_module(self, path):
53        """Determine which module the user is trying to test.
54
55        Returns the module to test. If there are multiple possibilities, will
56        ask the user. Otherwise will return the only module found.
57
58        Args:
59            path: String path of module to look for.
60
61        Returns:
62            A list of the module names.
63        """
64        testable_modules = []
65        for mod in self.module_info.get_module_names(path):
66            mod_info = self.module_info.get_module_info(mod)
67            # Robolectric tests always exist in pairs of 2, one module to build
68            # the test and another to run it. For now, we are assuming they are
69            # isolated in their own folders and will return if we find one.
70            if self.module_info.is_robolectric_test(mod):
71                # return a list with one module name if it is robolectric.
72                return [mod]
73            if self.module_info.is_testable_module(mod_info):
74                testable_modules.append(mod_info.get(constants.MODULE_NAME))
75        return test_finder_utils.extract_test_from_tests(testable_modules)
76
77    def _is_vts_module(self, module_name):
78        """Returns True if the module is a vts10 module, else False."""
79        mod_info = self.module_info.get_module_info(module_name)
80        suites = []
81        if mod_info:
82            suites = mod_info.get('compatibility_suites', [])
83        # Pull out all *ts (cts, tvts, etc) suites.
84        suites = [suite for suite in suites if suite not in _SUITES_TO_IGNORE]
85        return len(suites) == 1 and 'vts10' in suites
86
87    def _update_to_vts_test_info(self, test):
88        """Fill in the fields with vts10 specific info.
89
90        We need to update the runner to use the vts10 runner and also find the
91        test specific dependencies.
92
93        Args:
94            test: TestInfo to update with vts10 specific details.
95
96        Return:
97            TestInfo that is ready for the vts10 test runner.
98        """
99        test.test_runner = self._VTS_TEST_RUNNER
100        config_file = os.path.join(self.root_dir,
101                                   test.data[constants.TI_REL_CONFIG])
102        # Need to get out dir (special logic is to account for custom out dirs).
103        # The out dir is used to construct the build targets for the test deps.
104        out_dir = os.environ.get(constants.ANDROID_HOST_OUT)
105        custom_out_dir = os.environ.get(constants.ANDROID_OUT_DIR)
106        # If we're not an absolute custom out dir, get relative out dir path.
107        if custom_out_dir is None or not os.path.isabs(custom_out_dir):
108            out_dir = os.path.relpath(out_dir, self.root_dir)
109        vts_out_dir = os.path.join(out_dir, 'vts10', 'android-vts10', 'testcases')
110        # Parse dependency of default staging plans.
111        xml_paths = test_finder_utils.search_integration_dirs(
112            constants.VTS_STAGING_PLAN,
113            self.module_info.get_paths(constants.VTS_TF_MODULE))
114        vts_xmls = set()
115        vts_xmls.add(config_file)
116        for xml_path in xml_paths:
117            vts_xmls |= test_finder_utils.get_plans_from_vts_xml(xml_path)
118        for config_file in vts_xmls:
119            # Add in vts10 test build targets.
120            test.build_targets |= test_finder_utils.get_targets_from_vts_xml(
121                config_file, vts_out_dir, self.module_info)
122        test.build_targets.add('vts-test-core')
123        test.build_targets.add(test.test_name)
124        return test
125
126    def _update_to_robolectric_test_info(self, test):
127        """Update the fields for a robolectric test.
128
129        Args:
130          test: TestInfo to be updated with robolectric fields.
131
132        Returns:
133          TestInfo with robolectric fields.
134        """
135        test.test_runner = self._ROBOLECTRIC_RUNNER
136        test.test_name = self.module_info.get_robolectric_test_name(test.test_name)
137        return test
138
139    def _process_test_info(self, test):
140        """Process the test info and return some fields updated/changed.
141
142        We need to check if the test found is a special module (like vts10) and
143        update the test_info fields (like test_runner) appropriately.
144
145        Args:
146            test: TestInfo that has been filled out by a find method.
147
148        Return:
149            TestInfo that has been modified as needed and return None if
150            this module can't be found in the module_info.
151        """
152        module_name = test.test_name
153        mod_info = self.module_info.get_module_info(module_name)
154        if not mod_info:
155            return None
156        test.module_class = mod_info['class']
157        test.install_locations = test_finder_utils.get_install_locations(
158            mod_info['installed'])
159        # Check if this is only a vts10 module.
160        if self._is_vts_module(test.test_name):
161            return self._update_to_vts_test_info(test)
162        elif self.module_info.is_robolectric_test(test.test_name):
163            return self._update_to_robolectric_test_info(test)
164        rel_config = test.data[constants.TI_REL_CONFIG]
165        test.build_targets = self._get_build_targets(module_name, rel_config)
166        return test
167
168    def _get_build_targets(self, module_name, rel_config):
169        """Get the test deps.
170
171        Args:
172            module_name: name of the test.
173            rel_config: XML for the given test.
174
175        Returns:
176            Set of build targets.
177        """
178        targets = set()
179        if not self.module_info.is_auto_gen_test_config(module_name):
180            config_file = os.path.join(self.root_dir, rel_config)
181            targets = test_finder_utils.get_targets_from_xml(config_file,
182                                                             self.module_info)
183        if constants.VTS_CORE_SUITE in self.module_info.get_module_info(
184                module_name).get(constants.MODULE_COMPATIBILITY_SUITES, []):
185            targets.add(constants.VTS_CORE_TF_MODULE)
186        for module_path in self.module_info.get_paths(module_name):
187            mod_dir = module_path.replace('/', '-')
188            targets.add(_MODULES_IN % mod_dir)
189        # (b/156457698) Force add vts_kernel_tests as build target if our test
190        # belong to REQUIRED_KERNEL_TEST_MODULES due to required_module option
191        # not working for sh_test in soong.
192        if module_name in constants.REQUIRED_KERNEL_TEST_MODULES:
193            targets.add('vts_kernel_tests')
194        return targets
195
196    def _get_module_test_config(self, module_name, rel_config=None):
197        """Get the value of test_config in module_info.
198
199        Get the value of 'test_config' in module_info if its
200        auto_test_config is not true.
201        In this case, the test_config is specified by user.
202        If not, return rel_config.
203
204        Args:
205            module_name: A string of the test's module name.
206            rel_config: XML for the given test.
207
208        Returns:
209            A string of test_config path if found, else return rel_config.
210        """
211        mod_info = self.module_info.get_module_info(module_name)
212        if mod_info:
213            test_config = ''
214            test_config_list = mod_info.get(constants.MODULE_TEST_CONFIG, [])
215            if test_config_list:
216                test_config = test_config_list[0]
217            if not self.module_info.is_auto_gen_test_config(module_name) and test_config != '':
218                return test_config
219        return rel_config
220
221    def _get_test_info_filter(self, path, methods, **kwargs):
222        """Get test info filter.
223
224        Args:
225            path: A string of the test's path.
226            methods: A set of method name strings.
227            rel_module_dir: Optional. A string of the module dir relative to
228                root.
229            class_name: Optional. A string of the class name.
230            is_native_test: Optional. A boolean variable of whether to search
231                for a native test or not.
232
233        Returns:
234            A set of test info filter.
235        """
236        _, file_name = test_finder_utils.get_dir_path_and_filename(path)
237        ti_filter = frozenset()
238        if kwargs.get('is_native_test', None):
239            ti_filter = frozenset([test_info.TestFilter(
240                test_finder_utils.get_cc_filter(
241                    kwargs.get('class_name', '*'), methods), frozenset())])
242        # Path to java file.
243        elif file_name and constants.JAVA_EXT_RE.match(file_name):
244            full_class_name = test_finder_utils.get_fully_qualified_class_name(
245                path)
246            ti_filter = frozenset(
247                [test_info.TestFilter(full_class_name, methods)])
248        # Path to cc file.
249        elif file_name and constants.CC_EXT_RE.match(file_name):
250            if not test_finder_utils.has_cc_class(path):
251                raise atest_error.MissingCCTestCaseError(
252                    "Can't find CC class in %s" % path)
253            if methods:
254                ti_filter = frozenset(
255                    [test_info.TestFilter(test_finder_utils.get_cc_filter(
256                        kwargs.get('class_name', '*'), methods), frozenset())])
257        # Path to non-module dir, treat as package.
258        elif (not file_name
259              and kwargs.get('rel_module_dir', None) !=
260              os.path.relpath(path, self.root_dir)):
261            dir_items = [os.path.join(path, f) for f in os.listdir(path)]
262            for dir_item in dir_items:
263                if constants.JAVA_EXT_RE.match(dir_item):
264                    package_name = test_finder_utils.get_package_name(dir_item)
265                    if package_name:
266                        # methods should be empty frozenset for package.
267                        if methods:
268                            raise atest_error.MethodWithoutClassError(
269                                '%s: Method filtering requires class'
270                                % str(methods))
271                        ti_filter = frozenset(
272                            [test_info.TestFilter(package_name, methods)])
273                        break
274        return ti_filter
275
276    def _get_rel_config(self, test_path):
277        """Get config file's relative path.
278
279        Args:
280            test_path: A string of the test absolute path.
281
282        Returns:
283            A string of config's relative path, else None.
284        """
285        test_dir = os.path.dirname(test_path)
286        rel_module_dir = test_finder_utils.find_parent_module_dir(
287            self.root_dir, test_dir, self.module_info)
288        if rel_module_dir:
289            return os.path.join(rel_module_dir, constants.MODULE_CONFIG)
290        return None
291
292    def _get_test_infos(self, test_path, rel_config, module_name, test_filter):
293        """Get test_info for test_path.
294
295        Args:
296            test_path: A string of the test path.
297            rel_config: A string of rel path of config.
298            module_name: A string of the module name to use.
299            test_filter: A test info filter.
300
301        Returns:
302            A list of TestInfo namedtuple if found, else None.
303        """
304        if not rel_config:
305            rel_config = self._get_rel_config(test_path)
306            if not rel_config:
307                return None
308        if module_name:
309            module_names = [module_name]
310        else:
311            module_names = self._determine_testable_module(
312                os.path.dirname(rel_config))
313        test_infos = []
314        if module_names:
315            for mname in module_names:
316                # The real test config might be record in module-info.
317                rel_config = self._get_module_test_config(mname,
318                                                          rel_config=rel_config)
319                mod_info = self.module_info.get_module_info(mname)
320                tinfo = self._process_test_info(test_info.TestInfo(
321                    test_name=mname,
322                    test_runner=self._TEST_RUNNER,
323                    build_targets=set(),
324                    data={constants.TI_FILTER: test_filter,
325                          constants.TI_REL_CONFIG: rel_config},
326                    compatibility_suites=mod_info.get(
327                        constants.MODULE_COMPATIBILITY_SUITES, [])))
328                if tinfo:
329                    test_infos.append(tinfo)
330        return test_infos
331
332    def find_test_by_module_name(self, module_name):
333        """Find test for the given module name.
334
335        Args:
336            module_name: A string of the test's module name.
337
338        Returns:
339            A list that includes only 1 populated TestInfo namedtuple
340            if found, otherwise None.
341        """
342        mod_info = self.module_info.get_module_info(module_name)
343        if self.module_info.is_testable_module(mod_info):
344            # path is a list with only 1 element.
345            rel_config = os.path.join(mod_info['path'][0],
346                                      constants.MODULE_CONFIG)
347            rel_config = self._get_module_test_config(module_name, rel_config=rel_config)
348            tinfo = self._process_test_info(test_info.TestInfo(
349                test_name=module_name,
350                test_runner=self._TEST_RUNNER,
351                build_targets=set(),
352                data={constants.TI_REL_CONFIG: rel_config,
353                      constants.TI_FILTER: frozenset()},
354                compatibility_suites=mod_info.get(
355                    constants.MODULE_COMPATIBILITY_SUITES, [])))
356            if tinfo:
357                return [tinfo]
358        return None
359
360    def find_test_by_kernel_class_name(self, module_name, class_name):
361        """Find kernel test for the given class name.
362
363        Args:
364            module_name: A string of the module name to use.
365            class_name: A string of the test's class name.
366
367        Returns:
368            A list of populated TestInfo namedtuple if test found, else None.
369        """
370        class_name, methods = test_finder_utils.split_methods(class_name)
371        test_config = self._get_module_test_config(module_name)
372        test_config_path = os.path.join(self.root_dir, test_config)
373        mod_info = self.module_info.get_module_info(module_name)
374        ti_filter = frozenset(
375            [test_info.TestFilter(class_name, methods)])
376        if test_finder_utils.is_test_from_kernel_xml(test_config_path, class_name):
377            tinfo = self._process_test_info(test_info.TestInfo(
378                test_name=module_name,
379                test_runner=self._TEST_RUNNER,
380                build_targets=set(),
381                data={constants.TI_REL_CONFIG: test_config,
382                      constants.TI_FILTER: ti_filter},
383                compatibility_suites=mod_info.get(
384                    constants.MODULE_COMPATIBILITY_SUITES, [])))
385            if tinfo:
386                return [tinfo]
387        return None
388
389    def find_test_by_class_name(self, class_name, module_name=None,
390                                rel_config=None, is_native_test=False):
391        """Find test files given a class name.
392
393        If module_name and rel_config not given it will calculate it determine
394        it by looking up the tree from the class file.
395
396        Args:
397            class_name: A string of the test's class name.
398            module_name: Optional. A string of the module name to use.
399            rel_config: Optional. A string of module dir relative to repo root.
400            is_native_test: A boolean variable of whether to search for a
401            native test or not.
402
403        Returns:
404            A list of populated TestInfo namedtuple if test found, else None.
405        """
406        class_name, methods = test_finder_utils.split_methods(class_name)
407        if rel_config:
408            search_dir = os.path.join(self.root_dir,
409                                      os.path.dirname(rel_config))
410        else:
411            search_dir = self.root_dir
412        test_paths = test_finder_utils.find_class_file(search_dir, class_name,
413                                                       is_native_test, methods)
414        if not test_paths and rel_config:
415            logging.info('Did not find class (%s) under module path (%s), '
416                         'researching from repo root.', class_name, rel_config)
417            test_paths = test_finder_utils.find_class_file(self.root_dir,
418                                                           class_name,
419                                                           is_native_test,
420                                                           methods)
421        if not test_paths:
422            return None
423        tinfos = []
424        for test_path in test_paths:
425            test_filter = self._get_test_info_filter(
426                test_path, methods, class_name=class_name,
427                is_native_test=is_native_test)
428            tinfo = self._get_test_infos(test_path, rel_config,
429                                         module_name, test_filter)
430            if tinfo:
431                tinfos.extend(tinfo)
432        return tinfos
433
434    def find_test_by_module_and_class(self, module_class):
435        """Find the test info given a MODULE:CLASS string.
436
437        Args:
438            module_class: A string of form MODULE:CLASS or MODULE:CLASS#METHOD.
439
440        Returns:
441            A list of populated TestInfo namedtuple if found, else None.
442        """
443        if ':' not in module_class:
444            return None
445        module_name, class_name = module_class.split(':')
446        # module_infos is a list with at most 1 element.
447        module_infos = self.find_test_by_module_name(module_name)
448        module_info = module_infos[0] if module_infos else None
449        if not module_info:
450            return None
451        find_result = None
452        # If the target module is NATIVE_TEST, search CC classes only.
453        if not self.module_info.is_native_test(module_name):
454            # Find by java class.
455            find_result = self.find_test_by_class_name(
456                class_name, module_info.test_name,
457                module_info.data.get(constants.TI_REL_CONFIG))
458        # kernel target test is also define as NATIVE_TEST in build system.
459        # TODO (b/157210083) Update find_test_by_kernel_class_name method to
460        # support gen_rule use case.
461        if not find_result:
462            find_result = self.find_test_by_kernel_class_name(
463                module_name, class_name)
464        # Find by cc class.
465        if not find_result:
466            find_result = self.find_test_by_cc_class_name(
467                class_name, module_info.test_name,
468                module_info.data.get(constants.TI_REL_CONFIG))
469        return find_result
470
471    def find_test_by_package_name(self, package, module_name=None,
472                                  rel_config=None):
473        """Find the test info given a PACKAGE string.
474
475        Args:
476            package: A string of the package name.
477            module_name: Optional. A string of the module name.
478            ref_config: Optional. A string of rel path of config.
479
480        Returns:
481            A list of populated TestInfo namedtuple if found, else None.
482        """
483        _, methods = test_finder_utils.split_methods(package)
484        if methods:
485            raise atest_error.MethodWithoutClassError('%s: Method filtering '
486                                                      'requires class' % (
487                                                          methods))
488        # Confirm that packages exists and get user input for multiples.
489        if rel_config:
490            search_dir = os.path.join(self.root_dir,
491                                      os.path.dirname(rel_config))
492        else:
493            search_dir = self.root_dir
494        package_paths = test_finder_utils.run_find_cmd(
495            test_finder_utils.FIND_REFERENCE_TYPE.PACKAGE, search_dir, package)
496        # Package path will be the full path to the dir represented by package.
497        if not package_paths:
498            return None
499        test_filter = frozenset([test_info.TestFilter(package, frozenset())])
500        test_infos = []
501        for package_path in package_paths:
502            tinfo = self._get_test_infos(package_path, rel_config,
503                                         module_name, test_filter)
504            if tinfo:
505                test_infos.extend(tinfo)
506        return test_infos
507
508    def find_test_by_module_and_package(self, module_package):
509        """Find the test info given a MODULE:PACKAGE string.
510
511        Args:
512            module_package: A string of form MODULE:PACKAGE
513
514        Returns:
515            A list of populated TestInfo namedtuple if found, else None.
516        """
517        module_name, package = module_package.split(':')
518        # module_infos is a list with at most 1 element.
519        module_infos = self.find_test_by_module_name(module_name)
520        module_info = module_infos[0] if module_infos else None
521        if not module_info:
522            return None
523        return self.find_test_by_package_name(
524            package, module_info.test_name,
525            module_info.data.get(constants.TI_REL_CONFIG))
526
527    def find_test_by_path(self, path):
528        """Find the first test info matching the given path.
529
530        Strategy:
531            path_to_java_file --> Resolve to CLASS
532            path_to_cc_file --> Resolve to CC CLASS
533            path_to_module_file -> Resolve to MODULE
534            path_to_module_dir -> Resolve to MODULE
535            path_to_dir_with_class_files--> Resolve to PACKAGE
536            path_to_any_other_dir --> Resolve as MODULE
537
538        Args:
539            path: A string of the test's path.
540
541        Returns:
542            A list of populated TestInfo namedtuple if test found, else None
543        """
544        logging.debug('Finding test by path: %s', path)
545        path, methods = test_finder_utils.split_methods(path)
546        # TODO: See if this can be generalized and shared with methods above
547        # create absolute path from cwd and remove symbolic links
548        path = os.path.realpath(path)
549        if not os.path.exists(path):
550            return None
551        if (methods and
552                not test_finder_utils.has_method_in_file(path, methods)):
553            return None
554        dir_path, _ = test_finder_utils.get_dir_path_and_filename(path)
555        # Module/Class
556        rel_module_dir = test_finder_utils.find_parent_module_dir(
557            self.root_dir, dir_path, self.module_info)
558        if not rel_module_dir:
559            return None
560        rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG)
561        test_filter = self._get_test_info_filter(path, methods,
562                                                 rel_module_dir=rel_module_dir)
563        return self._get_test_infos(path, rel_config, None, test_filter)
564
565    def find_test_by_cc_class_name(self, class_name, module_name=None,
566                                   rel_config=None):
567        """Find test files given a cc class name.
568
569        If module_name and rel_config not given, test will be determined
570        by looking up the tree for files which has input class.
571
572        Args:
573            class_name: A string of the test's class name.
574            module_name: Optional. A string of the module name to use.
575            rel_config: Optional. A string of module dir relative to repo root.
576
577        Returns:
578            A list of populated TestInfo namedtuple if test found, else None.
579        """
580        # Check if class_name is prepended with file name. If so, trim the
581        # prefix and keep only the class_name.
582        if '.' in class_name:
583            # Assume the class name has a format of file_name.class_name
584            class_name = class_name[class_name.rindex('.')+1:]
585            logging.info('Search with updated class name: %s', class_name)
586        return self.find_test_by_class_name(
587            class_name, module_name, rel_config, is_native_test=True)
588
589    def get_testable_modules_with_ld(self, user_input, ld_range=0):
590        """Calculate the edit distances of the input and testable modules.
591
592        The user input will be calculated across all testable modules and
593        results in integers generated by Levenshtein Distance algorithm.
594        To increase the speed of the calculation, a bound can be applied to
595        this method to prevent from calculating every testable modules.
596
597        Guessing from typos, e.g. atest atest_unitests, implies a tangible range
598        of length that Atest only needs to search within it, and the default of
599        the bound is 2.
600
601        Guessing from keywords however, e.g. atest --search Camera, means that
602        the uncertainty of the module name is way higher, and Atest should walk
603        through all testable modules and return the highest possibilities.
604
605        Args:
606            user_input: A string of the user input.
607            ld_range: An integer that range the searching scope. If the length of
608                      user_input is 10, then Atest will calculate modules of which
609                      length is between 8 and 12. 0 is equivalent to unlimited.
610
611        Returns:
612            A List of LDs and possible module names. If the user_input is "fax",
613            the output will be like:
614            [[2, "fog"], [2, "Fix"], [4, "duck"], [7, "Duckies"]]
615
616            Which means the most lilely names of "fax" are fog and Fix(LD=2),
617            while Dickies is the most unlikely one(LD=7).
618        """
619        atest_utils.colorful_print('\nSearching for similar module names using '
620                                   'fuzzy search...', constants.CYAN)
621        testable_modules = sorted(self.module_info.get_testable_modules(), key=len)
622        lower_bound = len(user_input) - ld_range
623        upper_bound = len(user_input) + ld_range
624        testable_modules_with_ld = []
625        for module_name in testable_modules:
626            # Dispose those too short or too lengthy.
627            if ld_range != 0:
628                if len(module_name) < lower_bound:
629                    continue
630                elif len(module_name) > upper_bound:
631                    break
632            testable_modules_with_ld.append(
633                [test_finder_utils.get_levenshtein_distance(
634                    user_input, module_name), module_name])
635        return testable_modules_with_ld
636
637    def get_fuzzy_searching_results(self, user_input):
638        """Give results which have no more than allowance of edit distances.
639
640        Args:
641            user_input: the target module name for fuzzy searching.
642
643        Return:
644            A list of guessed modules.
645        """
646        modules_with_ld = self.get_testable_modules_with_ld(user_input,
647                                                            ld_range=constants.LD_RANGE)
648        guessed_modules = []
649        for _distance, _module in modules_with_ld:
650            if _distance <= abs(constants.LD_RANGE):
651                guessed_modules.append(_module)
652        return guessed_modules
653