blob: ec9a7a90b69ba173f2dac2220c0d578e7e197373 [file] [log] [blame]
Kevin Cheng8b2c94c2017-12-18 14:43:26 -08001# 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'
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080028
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)
easoncyleef0fb2b12019-01-22 15:49:09 +080049 self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080050
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, '/')
yangbill1067e482019-01-29 15:57:54 +080064 out_dir = os.environ.get(constants.ANDROID_PRODUCT_OUT, root_dir)
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080065 module_file_path = os.path.join(out_dir, _MODULE_INFO)
66
yangbill1067e482019-01-29 15:57:54 +080067 # 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:
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080070 # Make target is simply file path relative to root
71 module_info_target = os.path.relpath(module_file_path, root_dir)
72 else:
yangbill1067e482019-01-29 15:57:54 +080073 # 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!')
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080076 module_file_path = os.path.join(
yangbill1067e482019-01-29 15:57:54 +080077 os.environ.get(constants.ANDROID_PRODUCT_OUT), _MODULE_INFO)
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080078 module_info_target = module_file_path
79 if not os.path.isfile(module_file_path) or force_build:
nelsonli34997d52018-08-17 09:43:28 +080080 logging.debug('Generating %s - this is required for '
81 'initial runs.', _MODULE_INFO)
patricktu5456df02019-11-01 20:15:02 +080082 build_env = dict(constants.ATEST_BUILD_ENV)
83 build_env.update(constants.DEPS_LICENSE_ENV)
84 # Also build the deps-license module to generate dependencies data.
85 atest_utils.build([module_info_target, constants.DEPS_LICENSE],
Jim Tang6ed753e2019-07-23 10:39:58 +080086 verbose=logging.getLogger().isEnabledFor(logging.DEBUG),
patricktu5456df02019-11-01 20:15:02 +080087 env_vars=build_env)
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080088 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 = {}
yangbill78ef63e2018-08-31 00:11:25 +0800123 for mod_name, mod_info in name_to_module_info.items():
yangbill3573e7f2018-08-15 21:56:21 +0800124 # Cross-compiled and multi-arch modules actually all belong to
125 # a single target so filter out these extra modules.
yangbill2262b812018-08-28 20:46:03 +0800126 if mod_name != mod_info.get(constants.MODULE_NAME, ''):
yangbill3573e7f2018-08-15 21:56:21 +0800127 continue
Kevin Cheng5be930e2018-02-20 09:39:22 -0800128 for path in mod_info.get(constants.MODULE_PATH, []):
129 mod_info[constants.MODULE_NAME] = mod_name
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800130 # 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):
Kevin Cheng2f903be2018-03-05 10:30:26 -0800142 """Return paths of supplied module name, Empty list if non-existent."""
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800143 info = self.name_to_module_info.get(name)
144 if info:
Kevin Cheng5be930e2018-02-20 09:39:22 -0800145 return info.get(constants.MODULE_PATH, [])
Kevin Cheng2f903be2018-03-05 10:30:26 -0800146 return []
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800147
Kevin Cheng5be930e2018-02-20 09:39:22 -0800148 def get_module_names(self, rel_module_path):
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800149 """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:
Kevin Cheng5be930e2018-02-20 09:39:22 -0800155 List of module names.
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800156 """
Kevin Cheng5be930e2018-02-20 09:39:22 -0800157 return [m.get(constants.MODULE_NAME)
158 for m in self.path_to_module_info.get(rel_module_path, [])]
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800159
160 def get_module_info(self, mod_name):
161 """Return dict of info for given module name, None if non-existent."""
162 return self.name_to_module_info.get(mod_name)
easoncyleef0fb2b12019-01-22 15:49:09 +0800163
164 def is_suite_in_compatibility_suites(self, suite, mod_info):
165 """Check if suite exists in the compatibility_suites of module-info.
166
167 Args:
168 suite: A string of suite name.
169 mod_info: Dict of module info to check.
170
171 Returns:
172 True if it exists in mod_info, False otherwise.
173 """
174 return suite in mod_info.get(constants.MODULE_COMPATIBILITY_SUITES, [])
175
176 def get_testable_modules(self, suite=None):
177 """Return the testable modules of the given suite name.
178
179 Args:
180 suite: A string of suite name. Set to None to return all testable
181 modules.
182
183 Returns:
184 List of testable modules. Empty list if non-existent.
185 If suite is None, return all the testable modules in module-info.
186 """
187 modules = set()
188 for _, info in self.name_to_module_info.items():
189 if self.is_testable_module(info):
190 if suite:
191 if self.is_suite_in_compatibility_suites(suite, info):
192 modules.add(info.get(constants.MODULE_NAME))
193 else:
194 modules.add(info.get(constants.MODULE_NAME))
195 return modules
196
197 def is_testable_module(self, mod_info):
198 """Check if module is something we can test.
199
200 A module is testable if:
201 - it's installed, or
202 - it's a robolectric module (or shares path with one).
203
204 Args:
205 mod_info: Dict of module info to check.
206
207 Returns:
208 True if we can test this module, False otherwise.
209 """
210 if not mod_info:
211 return False
212 if mod_info.get(constants.MODULE_INSTALLED) and self.has_test_config(mod_info):
213 return True
214 if self.is_robolectric_test(mod_info.get(constants.MODULE_NAME)):
215 return True
216 return False
217
218 def has_test_config(self, mod_info):
219 """Validate if this module has a test config.
220
221 A module can have a test config in the following manner:
222 - AndroidTest.xml at the module path.
223 - test_config be set in module-info.json.
224 - Auto-generated config via the auto_test_config key in module-info.json.
225
226 Args:
227 mod_info: Dict of module info to check.
228
229 Returns:
230 True if this module has a test config, False otherwise.
231 """
232 # Check if test_config in module-info is set.
233 for test_config in mod_info.get(constants.MODULE_TEST_CONFIG, []):
234 if os.path.isfile(os.path.join(self.root_dir, test_config)):
235 return True
236 # Check for AndroidTest.xml at the module path.
237 for path in mod_info.get(constants.MODULE_PATH, []):
238 if os.path.isfile(os.path.join(self.root_dir, path,
239 constants.MODULE_CONFIG)):
240 return True
241 # Check if the module has an auto-generated config.
242 return self.is_auto_gen_test_config(mod_info.get(constants.MODULE_NAME))
243
244 def get_robolectric_test_name(self, module_name):
245 """Returns runnable robolectric module name.
246
247 There are at least 2 modules in every robolectric module path, return
248 the module that we can run as a build target.
249
250 Arg:
251 module_name: String of module.
252
253 Returns:
254 String of module that is the runnable robolectric module, None if
255 none could be found.
256 """
257 module_name_info = self.name_to_module_info.get(module_name)
258 if not module_name_info:
259 return None
260 module_paths = module_name_info.get(constants.MODULE_PATH, [])
261 if module_paths:
262 for mod in self.get_module_names(module_paths[0]):
263 mod_info = self.get_module_info(mod)
easoncyleebf56bcf2019-03-05 18:46:02 +0800264 if self.is_robolectric_module(mod_info):
easoncyleef0fb2b12019-01-22 15:49:09 +0800265 return mod
266 return None
267
268 def is_robolectric_test(self, module_name):
269 """Check if module is a robolectric test.
270
271 A module can be a robolectric test if the specified module has their
272 class set as ROBOLECTRIC (or shares their path with a module that does).
273
274 Args:
275 module_name: String of module to check.
276
277 Returns:
278 True if the module is a robolectric module, else False.
279 """
280 # Check 1, module class is ROBOLECTRIC
281 mod_info = self.get_module_info(module_name)
easoncyleebf56bcf2019-03-05 18:46:02 +0800282 if self.is_robolectric_module(mod_info):
easoncyleef0fb2b12019-01-22 15:49:09 +0800283 return True
284 # Check 2, shared modules in the path have class ROBOLECTRIC_CLASS.
285 if self.get_robolectric_test_name(module_name):
286 return True
287 return False
288
289 def is_auto_gen_test_config(self, module_name):
290 """Check if the test config file will be generated automatically.
291
292 Args:
293 module_name: A string of the module name.
294
295 Returns:
296 True if the test config file will be generated automatically.
297 """
298 if self.is_module(module_name):
299 mod_info = self.name_to_module_info.get(module_name)
300 auto_test_config = mod_info.get('auto_test_config', [])
301 return auto_test_config and auto_test_config[0]
302 return False
easoncyleebf56bcf2019-03-05 18:46:02 +0800303
304 def is_robolectric_module(self, mod_info):
305 """Check if a module is a robolectric module.
306
307 Args:
308 mod_info: ModuleInfo to check.
309
310 Returns:
311 True if module is a robolectric module, False otherwise.
312 """
313 if mod_info:
314 return (mod_info.get(constants.MODULE_CLASS, [None])[0] ==
315 constants.MODULE_CLASS_ROBOLECTRIC)
316 return False
yangbill57739f82019-03-28 18:25:43 +0800317
318 def is_native_test(self, module_name):
319 """Check if the input module is a native test.
320
321 Args:
322 module_name: A string of the module name.
323
324 Returns:
325 True if the test is a native test, False otherwise.
326 """
327 mod_info = self.get_module_info(module_name)
328 return constants.MODULE_CLASS_NATIVE_TESTS in mod_info.get(
329 constants.MODULE_CLASS, [])