Move indexing logic into Loader
Bug: 281547254
Bug: 263199608
Test: m clean && m atest && atest-dev atest_unittests
Test: m clean && m atest && atest-dev HelloWorldTests
Change-Id: Ied89fdb9cff58cbc3a8dae30a0f8ece1c9b709ed
diff --git a/atest/module_info.py b/atest/module_info.py
index 5a011df..a364efe 100644
--- a/atest/module_info.py
+++ b/atest/module_info.py
@@ -68,7 +68,7 @@
json.dump({}, f)
mi = load_from_file(module_file=f.name)
- mi.name_to_module_info.update(name_to_module_info)
+ mi.loader.name_to_module_info.update(name_to_module_info)
return mi
@@ -81,7 +81,7 @@
class Loader:
"""Class that handles load and merge processes."""
- def __init__(self, module_file=None, force_build: bool = False):
+ def __init__(self, module_file=None, force_build: bool=False, no_generate: bool=False):
self.java_dep_path = Path(
atest_utils.get_build_out_dir()).joinpath('soong', _JAVA_DEP_INFO)
self.cc_dep_path = Path(
@@ -99,7 +99,27 @@
# module_info_target stays None.
self.module_info_target = None
- def load_module_info_file(self, module_file):
+ if no_generate:
+ self.name_to_module_info = {}
+ return
+
+ self.name_to_module_info, self.path_to_module_info = self.load(
+ module_file)
+
+ # Index and checksum files that will be used.
+ index_dir = Path(os.getenv(constants.ANDROID_HOST_OUT, '')).joinpath('indexes')
+ self.module_index = index_dir.joinpath(constants.MODULE_INDEX)
+ self.module_index_proc = None
+
+ if self.update_merge_info or not self.module_index.is_file():
+ # Assumably null module_file reflects a common run, and index testable
+ # modules only when common runs.
+ if not module_file:
+ self.module_index_proc = atest_utils.run_multi_proc(
+ func=self._get_testable_modules,
+ kwargs={'index': True})
+
+ def load(self, module_file):
"""Load the module file.
No matter whether passing module_file or not, ModuleInfo will load
@@ -270,6 +290,102 @@
shutil.copy(temp_file.name, self.merged_dep_path)
return name_to_module_info
+ def get_testable_modules(self, suite=None):
+ """Return the testable modules of the given suite name.
+
+ Atest does not index testable modules against compatibility_suites. When
+ suite was given, or the index file was interrupted, always run
+ _get_testable_modules() and re-index.
+
+ Args:
+ suite: A string of suite name.
+
+ Returns:
+ If suite is not given, return all the testable modules in module
+ info, otherwise return only modules that belong to the suite.
+ """
+ modules = set()
+ start = time.time()
+ if self.module_index_proc:
+ self.module_index_proc.join()
+
+ if self.module_index.is_file():
+ if not suite:
+ with open(self.module_index, 'rb') as cache:
+ try:
+ modules = pickle.load(cache, encoding="utf-8")
+ except UnicodeDecodeError:
+ modules = pickle.load(cache)
+ # when module indexing was interrupted.
+ except EOFError:
+ pass
+ else:
+ modules = self._get_testable_modules(suite=suite)
+ # If the modules.idx does not exist or invalid for any reason, generate
+ # a new one arbitrarily.
+ if not modules:
+ if not suite:
+ modules = self._get_testable_modules(index=True)
+ else:
+ modules = self._get_testable_modules(index=True, suite=suite)
+ duration = time.time() - start
+ metrics.LocalDetectEvent(
+ detect_type=DetectType.TESTABLE_MODULES,
+ result=int(duration))
+ return modules
+
+ def _get_testable_modules(self, index=False, suite=None):
+ """Return all available testable modules and index them.
+
+ Args:
+ index: boolean that determines running _index_testable_modules().
+ suite: string for the suite name.
+
+ Returns:
+ Set of all testable modules.
+ """
+ modules = set()
+ begin = time.time()
+ for _, info in self.name_to_module_info.items():
+ if _is_testable_module(
+ self.name_to_module_info, self.path_to_module_info, info):
+
+ testable_module = info.get(constants.MODULE_NAME)
+ if testable_module:
+ modules.add(testable_module)
+
+ logging.debug('Probing all testable modules took %ss',
+ time.time() - begin)
+ if index:
+ self._index_testable_modules(modules)
+ if suite:
+ _modules = set()
+ for module_name in modules:
+ info = self.name_to_module_info.get(module_name)
+ if ModuleInfo.is_suite_in_compatibility_suites(suite, info):
+ testable_module = info.get(constants.MODULE_NAME)
+ if testable_module:
+ _modules.add(testable_module)
+ return _modules
+ return modules
+
+ def _index_testable_modules(self, content):
+ """Dump testable modules.
+
+ Args:
+ content: An object that will be written to the index file.
+ """
+ logging.debug(r'Indexing testable modules... '
+ r'(This is required whenever module-info.json '
+ r'was rebuilt.)')
+ Path(self.module_index).parent.mkdir(parents=True, exist_ok=True)
+ with open(self.module_index, 'wb') as cache:
+ try:
+ pickle.dump(content, cache, protocol=2)
+ except IOError:
+ logging.error('Failed in dumping %s', cache)
+ os.remove(cache)
+
class ModuleInfo:
"""Class that offers fast/easy lookup for Module related details."""
@@ -318,29 +434,19 @@
self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
self.roboleaf_tests = {}
- # Index and checksum files that will be used.
- index_dir = Path(os.getenv(constants.ANDROID_HOST_OUT, '')).joinpath('indexes')
- self.module_index = index_dir.joinpath(constants.MODULE_INDEX)
- self.module_index_proc = None
-
self.loader = Loader(
module_file=module_file,
force_build=force_build,
+ no_generate=no_generate,
)
- if no_generate:
- self.name_to_module_info = {}
- return
+ @property
+ def name_to_module_info(self):
+ return self.loader.name_to_module_info
- self.name_to_module_info, self.path_to_module_info = self.loader.load_module_info_file(
- module_file)
- if self.loader.update_merge_info or not self.module_index.is_file():
- # Assumably null module_file reflects a common run, and index testable
- # modules only when common runs.
- if not module_file:
- self.module_index_proc = atest_utils.run_multi_proc(
- func=self._get_testable_modules,
- kwargs={'index': True})
+ @property
+ def path_to_module_info(self):
+ return self.loader.path_to_module_info
@property
def module_info_target(self):
@@ -370,55 +476,6 @@
"""Caller of the same method in Loader class."""
return self.loader._save_module_info_timestamp()
- def _index_testable_modules(self, content):
- """Dump testable modules.
-
- Args:
- content: An object that will be written to the index file.
- """
- logging.debug(r'Indexing testable modules... '
- r'(This is required whenever module-info.json '
- r'was rebuilt.)')
- Path(self.module_index).parent.mkdir(parents=True, exist_ok=True)
- with open(self.module_index, 'wb') as cache:
- try:
- pickle.dump(content, cache, protocol=2)
- except IOError:
- logging.error('Failed in dumping %s', cache)
- os.remove(cache)
-
- def _get_testable_modules(self, index=False, suite=None):
- """Return all available testable modules and index them.
-
- Args:
- index: boolean that determines running _index_testable_modules().
- suite: string for the suite name.
-
- Returns:
- Set of all testable modules.
- """
- modules = set()
- begin = time.time()
- for _, info in self.name_to_module_info.items():
- if self.is_testable_module(info):
- testable_module = info.get(constants.MODULE_NAME)
- if testable_module:
- modules.add(testable_module)
- logging.debug('Probing all testable modules took %ss',
- time.time() - begin)
- if index:
- self._index_testable_modules(modules)
- if suite:
- _modules = set()
- for module_name in modules:
- info = self.get_module_info(module_name)
- if self.is_suite_in_compatibility_suites(suite, info):
- testable_module = info.get(constants.MODULE_NAME)
- if testable_module:
- _modules.add(testable_module)
- return _modules
- return modules
-
def is_module(self, name):
"""Return True if name is a module, False otherwise."""
info = self.get_module_info(name)
@@ -447,14 +504,14 @@
Returns:
List of module names.
"""
- return [m.get(constants.MODULE_NAME)
- for m in self.path_to_module_info.get(rel_module_path, [])]
+ return _get_module_names(self.path_to_module_info, rel_module_path)
def get_module_info(self, mod_name):
"""Return dict of info for given module name, None if non-existence."""
return self.name_to_module_info.get(mod_name)
- def is_suite_in_compatibility_suites(self, suite, mod_info):
+ @staticmethod
+ def is_suite_in_compatibility_suites(suite, mod_info):
"""Check if suite exists in the compatibility_suites of module-info.
Args:
@@ -470,58 +527,19 @@
constants.MODULE_COMPATIBILITY_SUITES, [])
def get_testable_modules(self, suite=None):
- """Return the testable modules of the given suite name.
+ return self.loader.get_testable_modules(suite)
- Atest does not index testable modules against compatibility_suites. When
- suite was given, or the index file was interrupted, always run
- _get_testable_modules() and re-index.
-
- Args:
- suite: A string of suite name.
-
- Returns:
- If suite is not given, return all the testable modules in module
- info, otherwise return only modules that belong to the suite.
- """
- modules = set()
- start = time.time()
- if self.module_index_proc:
- self.module_index_proc.join()
-
- if self.module_index.is_file():
- if not suite:
- with open(self.module_index, 'rb') as cache:
- try:
- modules = pickle.load(cache, encoding="utf-8")
- except UnicodeDecodeError:
- modules = pickle.load(cache)
- # when module indexing was interrupted.
- except EOFError:
- pass
- else:
- modules = self._get_testable_modules(suite=suite)
- # If the modules.idx does not exist or invalid for any reason, generate
- # a new one arbitrarily.
- if not modules:
- if not suite:
- modules = self._get_testable_modules(index=True)
- else:
- modules = self._get_testable_modules(index=True, suite=suite)
- duration = time.time() - start
- metrics.LocalDetectEvent(
- detect_type=DetectType.TESTABLE_MODULES,
- result=int(duration))
- return modules
-
- def is_tradefed_testable_module(self, info: Dict[str, Any]) -> bool:
+ @staticmethod
+ def is_tradefed_testable_module(info: Dict[str, Any]) -> bool:
"""Check whether the module is a Tradefed executable test."""
if not info:
return False
if not info.get(constants.MODULE_INSTALLED, []):
return False
- return self.has_test_config(info)
+ return ModuleInfo.has_test_config(info)
- def is_mobly_module(self, info: Dict[str, Any]) -> bool:
+ @staticmethod
+ def is_mobly_module(info: Dict[str, Any]) -> bool:
"""Check whether the module is a Mobly test.
Note: Only python_test_host modules marked with a test_options tag of
@@ -550,17 +568,11 @@
Returns:
True if we can test this module, False otherwise.
"""
- if not info:
- return False
- if self.is_tradefed_testable_module(info):
- return True
- if self.is_mobly_module(info):
- return True
- if self.is_legacy_robolectric_test(info):
- return True
- return False
+ return _is_testable_module(
+ self.name_to_module_info, self.path_to_module_info, info)
- def has_test_config(self, info: Dict[str, Any]) -> bool:
+ @staticmethod
+ def has_test_config(info: Dict[str, Any]) -> bool:
"""Validate if this module has a test config.
A module can have a test config in the following manner:
@@ -579,9 +591,8 @@
def is_legacy_robolectric_test(self, info: Dict[str, Any]) -> bool:
"""Return whether the module_name is a legacy Robolectric test"""
- if self.is_tradefed_testable_module(info):
- return False
- return bool(self.get_robolectric_test_name(info))
+ return _is_legacy_robolectric_test(
+ self.name_to_module_info, self.path_to_module_info, info)
def get_robolectric_test_name(self, info: Dict[str, Any]) -> str:
"""Returns runnable robolectric module name.
@@ -601,24 +612,8 @@
String of the first-matched associated module that belongs to the
actual robolectric module, None if nothing has been found.
"""
- if not info:
- return ''
- module_paths = info.get(constants.MODULE_PATH, [])
- if not module_paths:
- return ''
- filtered_module_names = [
- name
- for name in self.get_module_names(module_paths[0])
- if name.startswith("Run")
- ]
- return next(
- (
- name
- for name in filtered_module_names
- if self.is_legacy_robolectric_class(self.get_module_info(name))
- ),
- '',
- )
+ return _get_robolectric_test_name(
+ self.name_to_module_info, self.path_to_module_info, info)
def is_robolectric_test(self, module_name):
"""Check if the given module is a robolectric test.
@@ -784,7 +779,8 @@
return auto_test_config and auto_test_config[0]
return False
- def is_legacy_robolectric_class(self, info: Dict[str, Any]) -> bool:
+ @staticmethod
+ def is_legacy_robolectric_class(info: Dict[str, Any]) -> bool:
"""Check if the class is `ROBOLECTRIC`
This method is for legacy robolectric tests that the associated modules
@@ -1169,3 +1165,101 @@
else:
path_to_module_info[path] = [mod_info]
return path_to_module_info
+
+
+def _get_module_names(path_to_module_info, rel_module_path):
+ """Get the modules that all have module_path.
+
+ Args:
+ path_to_module_info: Dict of path to module info.
+ rel_module_path: path of module in module-info.json.
+
+ Returns:
+ List of module names.
+ """
+ return [m.get(constants.MODULE_NAME)
+ for m in path_to_module_info.get(rel_module_path, [])]
+
+
+def _get_robolectric_test_name(
+ name_to_module_info: Dict[str, Dict],
+ path_to_module_info: Dict[str, Dict],
+ info: Dict[str, Any]) -> str:
+ """Returns runnable robolectric module name.
+
+ This method is for legacy robolectric tests and returns one of associated
+ modules. The pattern is determined by the amount of shards:
+
+ 10 shards:
+ FooTests -> RunFooTests0, RunFooTests1 ... RunFooTests9
+ No shard:
+ FooTests -> RunFooTests
+
+ Arg:
+ name_to_module_info: Dict of name to module info.
+ path_to_module_info: Dict of path to module info.
+ info: Dict of module info to check.
+
+ Returns:
+ String of the first-matched associated module that belongs to the
+ actual robolectric module, None if nothing has been found.
+ """
+ if not info:
+ return ''
+ module_paths = info.get(constants.MODULE_PATH, [])
+ if not module_paths:
+ return ''
+ filtered_module_names = [
+ name
+ for name in _get_module_names(path_to_module_info, module_paths[0])
+ if name.startswith("Run")
+ ]
+ return next(
+ (
+ name
+ for name in filtered_module_names
+ if ModuleInfo.is_legacy_robolectric_class(name_to_module_info.get(name))
+ ),
+ '',
+ )
+
+
+def _is_legacy_robolectric_test(
+ name_to_module_info: Dict[str, Dict],
+ path_to_module_info: Dict[str, Dict],
+ info: Dict[str, Any]) -> bool:
+ """Return whether the module_name is a legacy Robolectric test"""
+ if ModuleInfo.is_tradefed_testable_module(info):
+ return False
+ return bool(_get_robolectric_test_name(
+ name_to_module_info, path_to_module_info, info))
+
+
+def _is_testable_module(
+ name_to_module_info: Dict[str, Dict],
+ path_to_module_info: Dict[str, Dict],
+ info: Dict[str, Any]) -> bool:
+ """Check if module is something we can test.
+
+ A module is testable if:
+ - it's a tradefed testable module, or
+ - it's a Mobly module, or
+ - it's a robolectric module (or shares path with one).
+
+ Args:
+ name_to_module_info: Dict of name to module info.
+ path_to_module_info: Dict of path to module info.
+ info: Dict of module info to check.
+
+ Returns:
+ True if we can test this module, False otherwise.
+ """
+ if not info:
+ return False
+ if ModuleInfo.is_tradefed_testable_module(info):
+ return True
+ if ModuleInfo.is_mobly_module(info):
+ return True
+ if _is_legacy_robolectric_test(name_to_module_info, path_to_module_info, info):
+ return True
+ return False
diff --git a/atest/module_info_unittest.py b/atest/module_info_unittest.py
index 00a48f5..bc4a493 100755
--- a/atest/module_info_unittest.py
+++ b/atest/module_info_unittest.py
@@ -150,7 +150,7 @@
self.assertEqual(custom_abs_out_dir_mod_targ,
mod_info.module_info_target)
- @mock.patch.object(module_info.Loader, 'load_module_info_file')
+ @mock.patch.object(module_info.Loader, 'load')
def test_get_path_to_module_info(self, mock_load_module):
"""Test that we correctly create the path to module info dict."""
mod_one = 'mod1'
@@ -221,7 +221,7 @@
@mock.patch.dict('os.environ', {constants.ANDROID_BUILD_TOP:'/',
constants.ANDROID_PRODUCT_OUT:PRODUCT_OUT_DIR,
constants.ANDROID_HOST_OUT:HOST_OUT_DIR})
- @mock.patch.object(module_info.ModuleInfo, 'is_testable_module')
+ @mock.patch('atest.module_info._is_testable_module', return_value=True)
@mock.patch.object(module_info.ModuleInfo, 'is_suite_in_compatibility_suites')
def test_get_testable_modules(self, mock_is_suite_exist, mock_is_testable):
"""Test get_testable_modules."""
@@ -234,8 +234,7 @@
self.assertTrue(expected_modules.issubset(mod_info.get_testable_modules()))
# 3. search modules by giving a suite name, run _get_testable_modules()
- mod_info.name_to_module_info = NAME_TO_MODULE_INFO
- mock_is_testable.return_value = True
+ mod_info = module_info.load_from_dict(name_to_module_info=NAME_TO_MODULE_INFO)
mock_is_suite_exist.return_value = True
self.assertEqual(1, len(mod_info.get_testable_modules('test_suite')))
mock_is_suite_exist.return_value = False
@@ -280,7 +279,7 @@
MOD_INFO_DICT[MOD_NAME2] = is_not_auto_test_config
MOD_INFO_DICT[MOD_NAME3] = is_not_auto_test_config_again
MOD_INFO_DICT[MOD_NAME4] = {}
- mod_info.name_to_module_info = MOD_INFO_DICT
+ mod_info.loader.name_to_module_info = MOD_INFO_DICT
self.assertTrue(mod_info.is_auto_gen_test_config(MOD_NAME1))
self.assertFalse(mod_info.is_auto_gen_test_config(MOD_NAME2))
self.assertFalse(mod_info.is_auto_gen_test_config(MOD_NAME3))
@@ -661,7 +660,7 @@
modules = modules or []
for m in modules:
- mod_info.name_to_module_info[m['module_name']] = m
+ mod_info.loader.name_to_module_info[m['module_name']] = m
for path in m['path']:
if path in mod_info.path_to_module_info:
mod_info.path_to_module_info[path].append(m)
diff --git a/atest/test_finders/module_finder_unittest.py b/atest/test_finders/module_finder_unittest.py
index 332c438..20fea6e 100755
--- a/atest/test_finders/module_finder_unittest.py
+++ b/atest/test_finders/module_finder_unittest.py
@@ -216,7 +216,7 @@
modules = modules or []
for m in modules:
- mod_info.name_to_module_info[m['module_name']] = m
+ mod_info.loader.name_to_module_info[m['module_name']] = m
for path in m['path']:
if path in mod_info.path_to_module_info:
mod_info.path_to_module_info[path].append(m)
@@ -1387,7 +1387,7 @@
modules = modules or []
for m in modules:
- mod_info.name_to_module_info[m['module_name']] = m
+ mod_info.loader.name_to_module_info[m['module_name']] = m
return mod_info