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