• 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 Info class used to hold cached module-info.json.
17"""
18
19# pylint: disable=line-too-long
20
21import json
22import logging
23import os
24import shutil
25import sys
26import tempfile
27import time
28
29import atest_utils
30import constants
31
32from metrics import metrics
33
34# JSON file generated by build system that lists all buildable targets.
35_MODULE_INFO = 'module-info.json'
36# JSON file generated by build system that lists dependencies for java.
37_JAVA_DEP_INFO = 'module_bp_java_deps.json'
38# JSON file generated by build system that lists dependencies for cc.
39_CC_DEP_INFO = 'module_bp_cc_deps.json'
40# JSON file generated by atest merged the content from module-info,
41# module_bp_java_deps.json, and module_bp_cc_deps.
42_MERGED_INFO = 'atest_merged_dep.json'
43
44class ModuleInfo:
45    """Class that offers fast/easy lookup for Module related details."""
46
47    def __init__(self, force_build=False, module_file=None):
48        """Initialize the ModuleInfo object.
49
50        Load up the module-info.json file and initialize the helper vars.
51
52        Args:
53            force_build: Boolean to indicate if we should rebuild the
54                         module_info file regardless if it's created or not.
55            module_file: String of path to file to load up. Used for testing.
56        """
57        module_info_target, name_to_module_info = self._load_module_info_file(
58            force_build, module_file)
59        self.name_to_module_info = name_to_module_info
60        self.module_info_target = module_info_target
61        self.path_to_module_info = self._get_path_to_module_info(
62            self.name_to_module_info)
63        self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
64
65    @staticmethod
66    def _discover_mod_file_and_target(force_build):
67        """Find the module file.
68
69        Args:
70            force_build: Boolean to indicate if we should rebuild the
71                         module_info file regardless if it's created or not.
72
73        Returns:
74            Tuple of module_info_target and path to module file.
75        """
76        logging.debug('Probing and validating module info...')
77        module_info_target = None
78        root_dir = os.environ.get(constants.ANDROID_BUILD_TOP, '/')
79        out_dir = os.environ.get(constants.ANDROID_PRODUCT_OUT, root_dir)
80        module_file_path = os.path.join(out_dir, _MODULE_INFO)
81
82        # Check if the user set a custom out directory by comparing the out_dir
83        # to the root_dir.
84        if out_dir.find(root_dir) == 0:
85            # Make target is simply file path no-absolute to root
86            module_info_target = os.path.relpath(module_file_path, root_dir)
87        else:
88            # If the user has set a custom out directory, generate an absolute
89            # path for module info targets.
90            logging.debug('User customized out dir!')
91            module_file_path = os.path.join(
92                os.environ.get(constants.ANDROID_PRODUCT_OUT), _MODULE_INFO)
93            module_info_target = module_file_path
94        # Make sure module-info exist and could be load properly.
95        if not atest_utils.is_valid_json_file(module_file_path) or force_build:
96            logging.debug('Generating %s - this is required for '
97                          'initial runs or forced rebuilds.', _MODULE_INFO)
98            build_env = dict(constants.ATEST_BUILD_ENV)
99            build_start = time.time()
100            if not atest_utils.build([module_info_target],
101                                     verbose=logging.getLogger().isEnabledFor(
102                                         logging.DEBUG), env_vars=build_env):
103                sys.exit(constants.EXIT_CODE_BUILD_FAILURE)
104            build_duration = time.time() - build_start
105            metrics.LocalDetectEvent(
106                detect_type=constants.DETECT_TYPE_ONLY_BUILD_MODULE_INFO,
107                result=int(build_duration))
108        return module_info_target, module_file_path
109
110    def _load_module_info_file(self, force_build, module_file):
111        """Load the module file.
112
113        Args:
114            force_build: Boolean to indicate if we should rebuild the
115                         module_info file regardless if it's created or not.
116            module_file: String of path to file to load up. Used for testing.
117
118        Returns:
119            Tuple of module_info_target and dict of json.
120        """
121        # If module_file is specified, we're testing so we don't care if
122        # module_info_target stays None.
123        module_info_target = None
124        file_path = module_file
125        if not file_path:
126            module_info_target, file_path = self._discover_mod_file_and_target(
127                force_build)
128        merged_file_path = self.get_atest_merged_info_path()
129        if (not self.need_update_merged_file(force_build)
130            and os.path.exists(merged_file_path)):
131            file_path = merged_file_path
132            logging.debug('Loading %s as module-info.', file_path)
133        with open(file_path) as json_file:
134            mod_info = json.load(json_file)
135        if self.need_update_merged_file(force_build):
136            mod_info = self._merge_build_system_infos(mod_info)
137        return module_info_target, mod_info
138
139    @staticmethod
140    def _get_path_to_module_info(name_to_module_info):
141        """Return the path_to_module_info dict.
142
143        Args:
144            name_to_module_info: Dict of module name to module info dict.
145
146        Returns:
147            Dict of module path to module info dict.
148        """
149        path_to_module_info = {}
150        for mod_name, mod_info in name_to_module_info.items():
151            # Cross-compiled and multi-arch modules actually all belong to
152            # a single target so filter out these extra modules.
153            if mod_name != mod_info.get(constants.MODULE_NAME, ''):
154                continue
155            for path in mod_info.get(constants.MODULE_PATH, []):
156                mod_info[constants.MODULE_NAME] = mod_name
157                # There could be multiple modules in a path.
158                if path in path_to_module_info:
159                    path_to_module_info[path].append(mod_info)
160                else:
161                    path_to_module_info[path] = [mod_info]
162        return path_to_module_info
163
164    def is_module(self, name):
165        """Return True if name is a module, False otherwise."""
166        if self.get_module_info(name):
167            return True
168        return False
169
170    def get_paths(self, name):
171        """Return paths of supplied module name, Empty list if non-existent."""
172        info = self.get_module_info(name)
173        if info:
174            return info.get(constants.MODULE_PATH, [])
175        return []
176
177    def get_module_names(self, rel_module_path):
178        """Get the modules that all have module_path.
179
180        Args:
181            rel_module_path: path of module in module-info.json
182
183        Returns:
184            List of module names.
185        """
186        return [m.get(constants.MODULE_NAME)
187                for m in self.path_to_module_info.get(rel_module_path, [])]
188
189    def get_module_info(self, mod_name):
190        """Return dict of info for given module name, None if non-existence."""
191        module_info = self.name_to_module_info.get(mod_name)
192        # Android's build system will automatically adding 2nd arch bitness
193        # string at the end of the module name which will make atest could not
194        # find the matched module. Rescan the module-info with the matched module
195        # name without bitness.
196        if not module_info:
197            for _, mod_info in self.name_to_module_info.items():
198                if mod_name == mod_info.get(constants.MODULE_NAME, ''):
199                    return mod_info
200        return module_info
201
202    def is_suite_in_compatibility_suites(self, suite, mod_info):
203        """Check if suite exists in the compatibility_suites of module-info.
204
205        Args:
206            suite: A string of suite name.
207            mod_info: Dict of module info to check.
208
209        Returns:
210            True if it exists in mod_info, False otherwise.
211        """
212        return suite in mod_info.get(constants.MODULE_COMPATIBILITY_SUITES, [])
213
214    def get_testable_modules(self, suite=None):
215        """Return the testable modules of the given suite name.
216
217        Args:
218            suite: A string of suite name. Set to None to return all testable
219            modules.
220
221        Returns:
222            List of testable modules. Empty list if non-existent.
223            If suite is None, return all the testable modules in module-info.
224        """
225        modules = set()
226        for _, info in self.name_to_module_info.items():
227            if self.is_testable_module(info):
228                if suite:
229                    if self.is_suite_in_compatibility_suites(suite, info):
230                        modules.add(info.get(constants.MODULE_NAME))
231                else:
232                    modules.add(info.get(constants.MODULE_NAME))
233        return modules
234
235    def is_testable_module(self, mod_info):
236        """Check if module is something we can test.
237
238        A module is testable if:
239          - it's installed, or
240          - it's a robolectric module (or shares path with one).
241
242        Args:
243            mod_info: Dict of module info to check.
244
245        Returns:
246            True if we can test this module, False otherwise.
247        """
248        if not mod_info:
249            return False
250        if mod_info.get(constants.MODULE_INSTALLED) and self.has_test_config(mod_info):
251            return True
252        if self.is_robolectric_test(mod_info.get(constants.MODULE_NAME)):
253            return True
254        return False
255
256    def has_test_config(self, mod_info):
257        """Validate if this module has a test config.
258
259        A module can have a test config in the following manner:
260          - AndroidTest.xml at the module path.
261          - test_config be set in module-info.json.
262          - Auto-generated config via the auto_test_config key
263            in module-info.json.
264
265        Args:
266            mod_info: Dict of module info to check.
267
268        Returns:
269            True if this module has a test config, False otherwise.
270        """
271        # Check if test_config in module-info is set.
272        for test_config in mod_info.get(constants.MODULE_TEST_CONFIG, []):
273            if os.path.isfile(os.path.join(self.root_dir, test_config)):
274                return True
275        # Check for AndroidTest.xml at the module path.
276        for path in mod_info.get(constants.MODULE_PATH, []):
277            if os.path.isfile(os.path.join(self.root_dir, path,
278                                           constants.MODULE_CONFIG)):
279                return True
280        # Check if the module has an auto-generated config.
281        return self.is_auto_gen_test_config(mod_info.get(constants.MODULE_NAME))
282
283    def get_robolectric_test_name(self, module_name):
284        """Returns runnable robolectric module name.
285
286        There are at least 2 modules in every robolectric module path, return
287        the module that we can run as a build target.
288
289        Arg:
290            module_name: String of module.
291
292        Returns:
293            String of module that is the runnable robolectric module, None if
294            none could be found.
295        """
296        module_name_info = self.get_module_info(module_name)
297        if not module_name_info:
298            return None
299        module_paths = module_name_info.get(constants.MODULE_PATH, [])
300        if module_paths:
301            for mod in self.get_module_names(module_paths[0]):
302                mod_info = self.get_module_info(mod)
303                if self.is_robolectric_module(mod_info):
304                    return mod
305        return None
306
307    def is_robolectric_test(self, module_name):
308        """Check if module is a robolectric test.
309
310        A module can be a robolectric test if the specified module has their
311        class set as ROBOLECTRIC (or shares their path with a module that does).
312
313        Args:
314            module_name: String of module to check.
315
316        Returns:
317            True if the module is a robolectric module, else False.
318        """
319        # Check 1, module class is ROBOLECTRIC
320        mod_info = self.get_module_info(module_name)
321        if self.is_robolectric_module(mod_info):
322            return True
323        # Check 2, shared modules in the path have class ROBOLECTRIC_CLASS.
324        if self.get_robolectric_test_name(module_name):
325            return True
326        return False
327
328    def is_auto_gen_test_config(self, module_name):
329        """Check if the test config file will be generated automatically.
330
331        Args:
332            module_name: A string of the module name.
333
334        Returns:
335            True if the test config file will be generated automatically.
336        """
337        if self.is_module(module_name):
338            mod_info = self.get_module_info(module_name)
339            auto_test_config = mod_info.get('auto_test_config', [])
340            return auto_test_config and auto_test_config[0]
341        return False
342
343    def is_robolectric_module(self, mod_info):
344        """Check if a module is a robolectric module.
345
346        Args:
347            mod_info: ModuleInfo to check.
348
349        Returns:
350            True if module is a robolectric module, False otherwise.
351        """
352        if mod_info:
353            return (mod_info.get(constants.MODULE_CLASS, [None])[0] ==
354                    constants.MODULE_CLASS_ROBOLECTRIC)
355        return False
356
357    def is_native_test(self, module_name):
358        """Check if the input module is a native test.
359
360        Args:
361            module_name: A string of the module name.
362
363        Returns:
364            True if the test is a native test, False otherwise.
365        """
366        mod_info = self.get_module_info(module_name)
367        return constants.MODULE_CLASS_NATIVE_TESTS in mod_info.get(
368            constants.MODULE_CLASS, [])
369
370    def has_mainline_modules(self, module_name, mainline_modules):
371        """Check if the mainline modules are in module-info.
372
373        Args:
374            module_name: A string of the module name.
375            mainline_modules: A list of mainline modules.
376
377        Returns:
378            True if mainline_modules is in module-info, False otherwise.
379        """
380        # TODO: (b/165425972)Check AndroidTest.xml or specific test config.
381        mod_info = self.get_module_info(module_name)
382        if mainline_modules in mod_info.get(constants.MODULE_MAINLINE_MODULES,
383                                            []):
384            return True
385        return False
386
387    def generate_atest_merged_dep_file(self):
388        """Method for generating atest_merged_dep.json."""
389        self._merge_build_system_infos(self.name_to_module_info,
390                                       self.get_java_dep_info_path(),
391                                       self.get_cc_dep_info_path())
392
393    def _merge_build_system_infos(self, name_to_module_info,
394        java_bp_info_path=None, cc_bp_info_path=None):
395        """Merge the full build system's info to name_to_module_info.
396
397        Args:
398            name_to_module_info: Dict of module name to module info dict.
399            java_bp_info_path: String of path to java dep file to load up.
400                               Used for testing.
401            cc_bp_info_path: String of path to cc dep file to load up.
402                             Used for testing.
403
404        Returns:
405            Dict of merged json of input def_file_path and name_to_module_info.
406        """
407        # Merge _JAVA_DEP_INFO
408        if not java_bp_info_path:
409            java_bp_info_path = self.get_java_dep_info_path()
410        if atest_utils.is_valid_json_file(java_bp_info_path):
411            with open(java_bp_info_path) as json_file:
412                java_bp_infos = json.load(json_file)
413                logging.debug('Merging Java build info: %s', java_bp_info_path)
414                name_to_module_info = self._merge_soong_info(
415                    name_to_module_info, java_bp_infos)
416        # Merge _CC_DEP_INFO
417        if not cc_bp_info_path:
418            cc_bp_info_path = self.get_cc_dep_info_path()
419        if atest_utils.is_valid_json_file(cc_bp_info_path):
420            with open(cc_bp_info_path) as json_file:
421                cc_bp_infos = json.load(json_file)
422            logging.debug('Merging CC build info: %s', cc_bp_info_path)
423            # CC's dep json format is different with java.
424            # Below is the example content:
425            # {
426            #   "clang": "${ANDROID_ROOT}/bin/clang",
427            #   "clang++": "${ANDROID_ROOT}/bin/clang++",
428            #   "modules": {
429            #       "ACameraNdkVendorTest": {
430            #           "path": [
431            #                   "frameworks/av/camera/ndk"
432            #           ],
433            #           "srcs": [
434            #                   "frameworks/tests/AImageVendorTest.cpp",
435            #                   "frameworks/tests/ACameraManagerTest.cpp"
436            #           ],
437            name_to_module_info = self._merge_soong_info(
438                name_to_module_info, cc_bp_infos.get('modules', {}))
439        return name_to_module_info
440
441    def _merge_soong_info(self, name_to_module_info, mod_bp_infos):
442        """Merge the dependency and srcs in mod_bp_infos to name_to_module_info.
443
444        Args:
445            name_to_module_info: Dict of module name to module info dict.
446            mod_bp_infos: Dict of module name to bp's module info dict.
447
448        Returns:
449            Dict of merged json of input def_file_path and name_to_module_info.
450        """
451        merge_items = [constants.MODULE_DEPENDENCIES, constants.MODULE_SRCS]
452        for module_name, dep_info in mod_bp_infos.items():
453            if name_to_module_info.get(module_name, None):
454                mod_info = name_to_module_info.get(module_name)
455                for merge_item in merge_items:
456                    dep_info_values = dep_info.get(merge_item, [])
457                    mod_info_values = mod_info.get(merge_item, [])
458                    for dep_info_value in dep_info_values:
459                        if dep_info_value not in mod_info_values:
460                            mod_info_values.append(dep_info_value)
461                    mod_info_values.sort()
462                    name_to_module_info[
463                        module_name][merge_item] = mod_info_values
464        output_file = self.get_atest_merged_info_path()
465        if not os.path.isdir(os.path.dirname(output_file)):
466            os.makedirs(os.path.dirname(output_file))
467        # b/178559543 saving merged module info in a temp file and copying it to
468        # atest_merged_dep.json can eliminate the possibility of accessing it
469        # concurrently and resulting in invalid JSON format.
470        temp_file = tempfile.NamedTemporaryFile()
471        with open(temp_file.name, 'w') as _temp:
472            json.dump(name_to_module_info, _temp, indent=0)
473        shutil.copy(temp_file.name, output_file)
474        temp_file.close()
475        return name_to_module_info
476
477    def get_module_dependency(self, module_name, depend_on=None):
478        """Get the dependency sets for input module.
479
480        Recursively find all the dependencies of the input module.
481
482        Args:
483            module_name: String of module to check.
484            depend_on: The list of parent dependencies.
485
486        Returns:
487            Set of dependency modules.
488        """
489        if not depend_on:
490            depend_on = set()
491        deps = set()
492        mod_info = self.get_module_info(module_name)
493        if not mod_info:
494            return deps
495        mod_deps = set(mod_info.get(constants.MODULE_DEPENDENCIES, []))
496        # Remove item in deps if it already in depend_on:
497        mod_deps = mod_deps - depend_on
498        deps = deps.union(mod_deps)
499        for mod_dep in mod_deps:
500            deps = deps.union(set(self.get_module_dependency(
501                mod_dep, depend_on=depend_on.union(deps))))
502        return deps
503
504    def get_install_module_dependency(self, module_name, depend_on=None):
505        """Get the dependency set for the given modules with installed path.
506
507        Args:
508            module_name: String of module to check.
509            depend_on: The list of parent dependencies.
510
511        Returns:
512            Set of dependency modules which has installed path.
513        """
514        install_deps = set()
515        deps = self.get_module_dependency(module_name, depend_on)
516        logging.debug('%s depends on: %s', module_name, deps)
517        for module in deps:
518            mod_info = self.get_module_info(module)
519            if mod_info and mod_info.get(constants.MODULE_INSTALLED, []):
520                install_deps.add(module)
521        logging.debug('modules %s required by %s were not installed',
522                      install_deps, module_name)
523        return install_deps
524
525    @staticmethod
526    def get_atest_merged_info_path():
527        """Returns the path for atest_merged_dep.json.
528
529        Returns:
530            String for atest_merged_dep.json.
531        """
532        return os.path.join(atest_utils.get_build_out_dir(),
533                            'soong', _MERGED_INFO)
534
535    @staticmethod
536    def get_java_dep_info_path():
537        """Returns the path for atest_merged_dep.json.
538
539        Returns:
540            String for atest_merged_dep.json.
541        """
542        return os.path.join(atest_utils.get_build_out_dir(),
543                            'soong', _JAVA_DEP_INFO)
544
545    @staticmethod
546    def get_cc_dep_info_path():
547        """Returns the path for atest_merged_dep.json.
548
549        Returns:
550            String for atest_merged_dep.json.
551        """
552        return os.path.join(atest_utils.get_build_out_dir(),
553                            'soong', _CC_DEP_INFO)
554
555    def has_soong_info(self):
556        """Ensure the existence of soong info files.
557
558        Returns:
559            True if soong info need to merge, false otherwise.
560        """
561        return (os.path.isfile(self.get_java_dep_info_path()) and
562                os.path.isfile(self.get_cc_dep_info_path()))
563
564    def need_update_merged_file(self, force_build=False):
565        """Check if need to update/generated atest_merged_dep.
566
567        If force_build, always update merged info.
568        If not force build, if soong info exist but merged inforamtion not exist,
569        need to update merged file.
570
571        Args:
572            force_build: Boolean to indicate that if user want to rebuild
573                         module_info file regardless if it's created or not.
574
575        Returns:
576            True if atest_merged_dep should be updated, false otherwise.
577        """
578        return (force_build or
579                (self.has_soong_info() and
580                 not os.path.exists(self.get_atest_merged_info_path())))
581
582    def is_unit_test(self, mod_info):
583        """Return True if input module is unit test, False otherwise.
584
585        Args:
586            mod_info: ModuleInfo to check.
587
588        Returns:
589            True if if input module is unit test, False otherwise.
590        """
591        return mod_info.get(constants.MODULE_IS_UNIT_TEST, '') == 'true'
592
593    def get_all_unit_tests(self):
594        """Get a list of all the module names which are unit tests."""
595        unit_tests = []
596        for mod_name, mod_info in self.name_to_module_info.items():
597            if mod_info.get(constants.MODULE_NAME, '') == mod_name:
598                if self.is_unit_test(mod_info):
599                    unit_tests.append(mod_name)
600        return unit_tests
601