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