blob: 5e0104aee5d8ae7bb3bbdb5595cf8fff777e64e5 [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)
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080082 atest_utils.build([module_info_target],
Jim Tang6ed753e2019-07-23 10:39:58 +080083 verbose=logging.getLogger().isEnabledFor(logging.DEBUG),
yangbill4b618ed2019-07-23 16:03:38 +080084 env_vars=constants.ATEST_BUILD_ENV)
Kevin Cheng8b2c94c2017-12-18 14:43:26 -080085 return module_info_target, module_file_path
86
87 def _load_module_info_file(self, force_build, module_file):
88 """Load the module file.
89
90 Args:
91 force_build: Boolean to indicate if we should rebuild the
92 module_info file regardless if it's created or not.
93 module_file: String of path to file to load up. Used for testing.
94
95 Returns:
96 Tuple of module_info_target and dict of json.
97 """
98 # If module_file is specified, we're testing so we don't care if
99 # module_info_target stays None.
100 module_info_target = None
101 file_path = module_file
102 if not file_path:
103 module_info_target, file_path = self._discover_mod_file_and_target(
104 force_build)
105 with open(file_path) as json_file:
106 mod_info = json.load(json_file)
107 return module_info_target, mod_info
108
109 @staticmethod
110 def _get_path_to_module_info(name_to_module_info):
111 """Return the path_to_module_info dict.
112
113 Args:
114 name_to_module_info: Dict of module name to module info dict.
115
116 Returns:
117 Dict of module path to module info dict.
118 """
119 path_to_module_info = {}
yangbill78ef63e2018-08-31 00:11:25 +0800120 for mod_name, mod_info in name_to_module_info.items():
yangbill3573e7f2018-08-15 21:56:21 +0800121 # Cross-compiled and multi-arch modules actually all belong to
122 # a single target so filter out these extra modules.
yangbill2262b812018-08-28 20:46:03 +0800123 if mod_name != mod_info.get(constants.MODULE_NAME, ''):
yangbill3573e7f2018-08-15 21:56:21 +0800124 continue
Kevin Cheng5be930e2018-02-20 09:39:22 -0800125 for path in mod_info.get(constants.MODULE_PATH, []):
126 mod_info[constants.MODULE_NAME] = mod_name
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800127 # There could be multiple modules in a path.
128 if path in path_to_module_info:
129 path_to_module_info[path].append(mod_info)
130 else:
131 path_to_module_info[path] = [mod_info]
132 return path_to_module_info
133
134 def is_module(self, name):
135 """Return True if name is a module, False otherwise."""
136 return name in self.name_to_module_info
137
138 def get_paths(self, name):
Kevin Cheng2f903be2018-03-05 10:30:26 -0800139 """Return paths of supplied module name, Empty list if non-existent."""
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800140 info = self.name_to_module_info.get(name)
141 if info:
Kevin Cheng5be930e2018-02-20 09:39:22 -0800142 return info.get(constants.MODULE_PATH, [])
Kevin Cheng2f903be2018-03-05 10:30:26 -0800143 return []
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800144
Kevin Cheng5be930e2018-02-20 09:39:22 -0800145 def get_module_names(self, rel_module_path):
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800146 """Get the modules that all have module_path.
147
148 Args:
149 rel_module_path: path of module in module-info.json
150
151 Returns:
Kevin Cheng5be930e2018-02-20 09:39:22 -0800152 List of module names.
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800153 """
Kevin Cheng5be930e2018-02-20 09:39:22 -0800154 return [m.get(constants.MODULE_NAME)
155 for m in self.path_to_module_info.get(rel_module_path, [])]
Kevin Cheng8b2c94c2017-12-18 14:43:26 -0800156
157 def get_module_info(self, mod_name):
158 """Return dict of info for given module name, None if non-existent."""
159 return self.name_to_module_info.get(mod_name)
easoncyleef0fb2b12019-01-22 15:49:09 +0800160
161 def is_suite_in_compatibility_suites(self, suite, mod_info):
162 """Check if suite exists in the compatibility_suites of module-info.
163
164 Args:
165 suite: A string of suite name.
166 mod_info: Dict of module info to check.
167
168 Returns:
169 True if it exists in mod_info, False otherwise.
170 """
171 return suite in mod_info.get(constants.MODULE_COMPATIBILITY_SUITES, [])
172
173 def get_testable_modules(self, suite=None):
174 """Return the testable modules of the given suite name.
175
176 Args:
177 suite: A string of suite name. Set to None to return all testable
178 modules.
179
180 Returns:
181 List of testable modules. Empty list if non-existent.
182 If suite is None, return all the testable modules in module-info.
183 """
184 modules = set()
185 for _, info in self.name_to_module_info.items():
186 if self.is_testable_module(info):
187 if suite:
188 if self.is_suite_in_compatibility_suites(suite, info):
189 modules.add(info.get(constants.MODULE_NAME))
190 else:
191 modules.add(info.get(constants.MODULE_NAME))
192 return modules
193
194 def is_testable_module(self, mod_info):
195 """Check if module is something we can test.
196
197 A module is testable if:
198 - it's installed, or
199 - it's a robolectric module (or shares path with one).
200
201 Args:
202 mod_info: Dict of module info to check.
203
204 Returns:
205 True if we can test this module, False otherwise.
206 """
207 if not mod_info:
208 return False
209 if mod_info.get(constants.MODULE_INSTALLED) and self.has_test_config(mod_info):
210 return True
211 if self.is_robolectric_test(mod_info.get(constants.MODULE_NAME)):
212 return True
213 return False
214
215 def has_test_config(self, mod_info):
216 """Validate if this module has a test config.
217
218 A module can have a test config in the following manner:
219 - AndroidTest.xml at the module path.
220 - test_config be set in module-info.json.
221 - Auto-generated config via the auto_test_config key in module-info.json.
222
223 Args:
224 mod_info: Dict of module info to check.
225
226 Returns:
227 True if this module has a test config, False otherwise.
228 """
229 # Check if test_config in module-info is set.
230 for test_config in mod_info.get(constants.MODULE_TEST_CONFIG, []):
231 if os.path.isfile(os.path.join(self.root_dir, test_config)):
232 return True
233 # Check for AndroidTest.xml at the module path.
234 for path in mod_info.get(constants.MODULE_PATH, []):
235 if os.path.isfile(os.path.join(self.root_dir, path,
236 constants.MODULE_CONFIG)):
237 return True
238 # Check if the module has an auto-generated config.
239 return self.is_auto_gen_test_config(mod_info.get(constants.MODULE_NAME))
240
241 def get_robolectric_test_name(self, module_name):
242 """Returns runnable robolectric module name.
243
244 There are at least 2 modules in every robolectric module path, return
245 the module that we can run as a build target.
246
247 Arg:
248 module_name: String of module.
249
250 Returns:
251 String of module that is the runnable robolectric module, None if
252 none could be found.
253 """
254 module_name_info = self.name_to_module_info.get(module_name)
255 if not module_name_info:
256 return None
257 module_paths = module_name_info.get(constants.MODULE_PATH, [])
258 if module_paths:
259 for mod in self.get_module_names(module_paths[0]):
260 mod_info = self.get_module_info(mod)
easoncyleebf56bcf2019-03-05 18:46:02 +0800261 if self.is_robolectric_module(mod_info):
easoncyleef0fb2b12019-01-22 15:49:09 +0800262 return mod
263 return None
264
265 def is_robolectric_test(self, module_name):
266 """Check if module is a robolectric test.
267
268 A module can be a robolectric test if the specified module has their
269 class set as ROBOLECTRIC (or shares their path with a module that does).
270
271 Args:
272 module_name: String of module to check.
273
274 Returns:
275 True if the module is a robolectric module, else False.
276 """
277 # Check 1, module class is ROBOLECTRIC
278 mod_info = self.get_module_info(module_name)
easoncyleebf56bcf2019-03-05 18:46:02 +0800279 if self.is_robolectric_module(mod_info):
easoncyleef0fb2b12019-01-22 15:49:09 +0800280 return True
281 # Check 2, shared modules in the path have class ROBOLECTRIC_CLASS.
282 if self.get_robolectric_test_name(module_name):
283 return True
284 return False
285
286 def is_auto_gen_test_config(self, module_name):
287 """Check if the test config file will be generated automatically.
288
289 Args:
290 module_name: A string of the module name.
291
292 Returns:
293 True if the test config file will be generated automatically.
294 """
295 if self.is_module(module_name):
296 mod_info = self.name_to_module_info.get(module_name)
297 auto_test_config = mod_info.get('auto_test_config', [])
298 return auto_test_config and auto_test_config[0]
299 return False
easoncyleebf56bcf2019-03-05 18:46:02 +0800300
301 def is_robolectric_module(self, mod_info):
302 """Check if a module is a robolectric module.
303
304 Args:
305 mod_info: ModuleInfo to check.
306
307 Returns:
308 True if module is a robolectric module, False otherwise.
309 """
310 if mod_info:
311 return (mod_info.get(constants.MODULE_CLASS, [None])[0] ==
312 constants.MODULE_CLASS_ROBOLECTRIC)
313 return False
yangbill57739f82019-03-28 18:25:43 +0800314
315 def is_native_test(self, module_name):
316 """Check if the input module is a native test.
317
318 Args:
319 module_name: A string of the module name.
320
321 Returns:
322 True if the test is a native test, False otherwise.
323 """
324 mod_info = self.get_module_info(module_name)
325 return constants.MODULE_CLASS_NATIVE_TESTS in mod_info.get(
326 constants.MODULE_CLASS, [])