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
24
25import atest_utils
26import constants
27
28# JSON file generated by build system that lists all buildable targets.
29_MODULE_INFO = 'module-info.json'
30
31
32class ModuleInfo:
33    """Class that offers fast/easy lookup for Module related details."""
34
35    def __init__(self, force_build=False, module_file=None):
36        """Initialize the ModuleInfo object.
37
38        Load up the module-info.json file and initialize the helper vars.
39
40        Args:
41            force_build: Boolean to indicate if we should rebuild the
42                         module_info file regardless if it's created or not.
43            module_file: String of path to file to load up. Used for testing.
44        """
45        module_info_target, name_to_module_info = self._load_module_info_file(
46            force_build, module_file)
47        self.name_to_module_info = name_to_module_info
48        self.module_info_target = module_info_target
49        self.path_to_module_info = self._get_path_to_module_info(
50            self.name_to_module_info)
51        self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
52
53    @staticmethod
54    def _discover_mod_file_and_target(force_build):
55        """Find the module file.
56
57        Args:
58            force_build: Boolean to indicate if we should rebuild the
59                         module_info file regardless if it's created or not.
60
61        Returns:
62            Tuple of module_info_target and path to module file.
63        """
64        module_info_target = None
65        root_dir = os.environ.get(constants.ANDROID_BUILD_TOP, '/')
66        out_dir = os.environ.get(constants.ANDROID_PRODUCT_OUT, root_dir)
67        module_file_path = os.path.join(out_dir, _MODULE_INFO)
68
69        # Check if the user set a custom out directory by comparing the out_dir
70        # to the root_dir.
71        if out_dir.find(root_dir) == 0:
72            # Make target is simply file path no-absolute to root
73            module_info_target = os.path.relpath(module_file_path, root_dir)
74        else:
75            # If the user has set a custom out directory, generate an absolute
76            # path for module info targets.
77            logging.debug('User customized out dir!')
78            module_file_path = os.path.join(
79                os.environ.get(constants.ANDROID_PRODUCT_OUT), _MODULE_INFO)
80            module_info_target = module_file_path
81        if not os.path.isfile(module_file_path) or force_build:
82            logging.debug('Generating %s - this is required for '
83                          'initial runs.', _MODULE_INFO)
84            build_env = dict(constants.ATEST_BUILD_ENV)
85            atest_utils.build([module_info_target],
86                              verbose=logging.getLogger().isEnabledFor(
87                                  logging.DEBUG), env_vars=build_env)
88        return module_info_target, module_file_path
89
90    def _load_module_info_file(self, force_build, module_file):
91        """Load the module file.
92
93        Args:
94            force_build: Boolean to indicate if we should rebuild the
95                         module_info file regardless if it's created or not.
96            module_file: String of path to file to load up. Used for testing.
97
98        Returns:
99            Tuple of module_info_target and dict of json.
100        """
101        # If module_file is specified, we're testing so we don't care if
102        # module_info_target stays None.
103        module_info_target = None
104        file_path = module_file
105        if not file_path:
106            module_info_target, file_path = self._discover_mod_file_and_target(
107                force_build)
108        with open(file_path) as json_file:
109            mod_info = json.load(json_file)
110        return module_info_target, mod_info
111
112    @staticmethod
113    def _get_path_to_module_info(name_to_module_info):
114        """Return the path_to_module_info dict.
115
116        Args:
117            name_to_module_info: Dict of module name to module info dict.
118
119        Returns:
120            Dict of module path to module info dict.
121        """
122        path_to_module_info = {}
123        for mod_name, mod_info in name_to_module_info.items():
124            # Cross-compiled and multi-arch modules actually all belong to
125            # a single target so filter out these extra modules.
126            if mod_name != mod_info.get(constants.MODULE_NAME, ''):
127                continue
128            for path in mod_info.get(constants.MODULE_PATH, []):
129                mod_info[constants.MODULE_NAME] = mod_name
130                # There could be multiple modules in a path.
131                if path in path_to_module_info:
132                    path_to_module_info[path].append(mod_info)
133                else:
134                    path_to_module_info[path] = [mod_info]
135        return path_to_module_info
136
137    def is_module(self, name):
138        """Return True if name is a module, False otherwise."""
139        return name in self.name_to_module_info
140
141    def get_paths(self, name):
142        """Return paths of supplied module name, Empty list if non-existent."""
143        info = self.name_to_module_info.get(name)
144        if info:
145            return info.get(constants.MODULE_PATH, [])
146        return []
147
148    def get_module_names(self, rel_module_path):
149        """Get the modules that all have module_path.
150
151        Args:
152            rel_module_path: path of module in module-info.json
153
154        Returns:
155            List of module names.
156        """
157        return [m.get(constants.MODULE_NAME)
158                for m in self.path_to_module_info.get(rel_module_path, [])]
159
160    def get_module_info(self, mod_name):
161        """Return dict of info for given module name, None if non-existent."""
162        module_info = self.name_to_module_info.get(mod_name)
163        # Android's build system will automatically adding 2nd arch bitness
164        # string at the end of the module name which will make atest could not
165        # finding matched module. Rescan the module-info with matched module
166        # name without bitness.
167        if not module_info:
168            for _, module_info in self.name_to_module_info.items():
169                if mod_name == module_info.get(constants.MODULE_NAME, ''):
170                    break
171        return module_info
172
173    def is_suite_in_compatibility_suites(self, suite, mod_info):
174        """Check if suite exists in the compatibility_suites of module-info.
175
176        Args:
177            suite: A string of suite name.
178            mod_info: Dict of module info to check.
179
180        Returns:
181            True if it exists in mod_info, False otherwise.
182        """
183        return suite in mod_info.get(constants.MODULE_COMPATIBILITY_SUITES, [])
184
185    def get_testable_modules(self, suite=None):
186        """Return the testable modules of the given suite name.
187
188        Args:
189            suite: A string of suite name. Set to None to return all testable
190            modules.
191
192        Returns:
193            List of testable modules. Empty list if non-existent.
194            If suite is None, return all the testable modules in module-info.
195        """
196        modules = set()
197        for _, info in self.name_to_module_info.items():
198            if self.is_testable_module(info):
199                if suite:
200                    if self.is_suite_in_compatibility_suites(suite, info):
201                        modules.add(info.get(constants.MODULE_NAME))
202                else:
203                    modules.add(info.get(constants.MODULE_NAME))
204        return modules
205
206    def is_testable_module(self, mod_info):
207        """Check if module is something we can test.
208
209        A module is testable if:
210          - it's installed, or
211          - it's a robolectric module (or shares path with one).
212
213        Args:
214            mod_info: Dict of module info to check.
215
216        Returns:
217            True if we can test this module, False otherwise.
218        """
219        if not mod_info:
220            return False
221        if mod_info.get(constants.MODULE_INSTALLED) and self.has_test_config(mod_info):
222            return True
223        if self.is_robolectric_test(mod_info.get(constants.MODULE_NAME)):
224            return True
225        return False
226
227    def has_test_config(self, mod_info):
228        """Validate if this module has a test config.
229
230        A module can have a test config in the following manner:
231          - AndroidTest.xml at the module path.
232          - test_config be set in module-info.json.
233          - Auto-generated config via the auto_test_config key
234            in module-info.json.
235
236        Args:
237            mod_info: Dict of module info to check.
238
239        Returns:
240            True if this module has a test config, False otherwise.
241        """
242        # Check if test_config in module-info is set.
243        for test_config in mod_info.get(constants.MODULE_TEST_CONFIG, []):
244            if os.path.isfile(os.path.join(self.root_dir, test_config)):
245                return True
246        # Check for AndroidTest.xml at the module path.
247        for path in mod_info.get(constants.MODULE_PATH, []):
248            if os.path.isfile(os.path.join(self.root_dir, path,
249                                           constants.MODULE_CONFIG)):
250                return True
251        # Check if the module has an auto-generated config.
252        return self.is_auto_gen_test_config(mod_info.get(constants.MODULE_NAME))
253
254    def get_robolectric_test_name(self, module_name):
255        """Returns runnable robolectric module name.
256
257        There are at least 2 modules in every robolectric module path, return
258        the module that we can run as a build target.
259
260        Arg:
261            module_name: String of module.
262
263        Returns:
264            String of module that is the runnable robolectric module, None if
265            none could be found.
266        """
267        module_name_info = self.name_to_module_info.get(module_name)
268        if not module_name_info:
269            return None
270        module_paths = module_name_info.get(constants.MODULE_PATH, [])
271        if module_paths:
272            for mod in self.get_module_names(module_paths[0]):
273                mod_info = self.get_module_info(mod)
274                if self.is_robolectric_module(mod_info):
275                    return mod
276        return None
277
278    def is_robolectric_test(self, module_name):
279        """Check if module is a robolectric test.
280
281        A module can be a robolectric test if the specified module has their
282        class set as ROBOLECTRIC (or shares their path with a module that does).
283
284        Args:
285            module_name: String of module to check.
286
287        Returns:
288            True if the module is a robolectric module, else False.
289        """
290        # Check 1, module class is ROBOLECTRIC
291        mod_info = self.get_module_info(module_name)
292        if self.is_robolectric_module(mod_info):
293            return True
294        # Check 2, shared modules in the path have class ROBOLECTRIC_CLASS.
295        if self.get_robolectric_test_name(module_name):
296            return True
297        return False
298
299    def is_auto_gen_test_config(self, module_name):
300        """Check if the test config file will be generated automatically.
301
302        Args:
303            module_name: A string of the module name.
304
305        Returns:
306            True if the test config file will be generated automatically.
307        """
308        if self.is_module(module_name):
309            mod_info = self.name_to_module_info.get(module_name)
310            auto_test_config = mod_info.get('auto_test_config', [])
311            return auto_test_config and auto_test_config[0]
312        return False
313
314    def is_robolectric_module(self, mod_info):
315        """Check if a module is a robolectric module.
316
317        Args:
318            mod_info: ModuleInfo to check.
319
320        Returns:
321            True if module is a robolectric module, False otherwise.
322        """
323        if mod_info:
324            return (mod_info.get(constants.MODULE_CLASS, [None])[0] ==
325                    constants.MODULE_CLASS_ROBOLECTRIC)
326        return False
327
328    def is_native_test(self, module_name):
329        """Check if the input module is a native test.
330
331        Args:
332            module_name: A string of the module name.
333
334        Returns:
335            True if the test is a native test, False otherwise.
336        """
337        mod_info = self.get_module_info(module_name)
338        return constants.MODULE_CLASS_NATIVE_TESTS in mod_info.get(
339            constants.MODULE_CLASS, [])
340