Create ModuleInfo by Loader

Bug: 281547254
Bug: 263199608
Test: m clean && m atest && atest-dev atest_unittests
Test: m clean && m atest && atest-dev HelloWorldTests
Test: m clean && m atest && atest-dev cts/tests/tests/media/misc/src/android/media/misc/cts/ResourceManagerTest.java
Test: m clean && m atest && atest-dev cts/tests/tests/gesture
Test: m clean && m atest && atest-dev --list-modules cts

Change-Id: Iee200ae235f99cd64f90f5b6a7643809d4ef53ec
diff --git a/atest/atest_main.py b/atest/atest_main.py
index 2097980..6f6d146 100755
--- a/atest/atest_main.py
+++ b/atest/atest_main.py
@@ -1058,15 +1058,8 @@
             proc_idx = atest_utils.run_multi_proc(at.index_targets)
         smart_rebuild = need_rebuild_module_info(args)
 
-        mod_start = time.time()
-        mod_info = module_info.load_from_file(force_build=smart_rebuild)
-        mod_stop = time.time() - mod_start
-        metrics.LocalDetectEvent(detect_type=DetectType.MODULE_INFO_INIT_MS,
-                                 result=int(mod_stop * 1000))
-        atest_utils.run_multi_proc(func=mod_info._save_module_info_timestamp)
-        atest_utils.run_multi_proc(
-            func=atest_utils.save_build_files_timestamp,
-        )
+        mod_info = module_info.load_from_file(
+            force_build=smart_rebuild, save_timestamps=True)
 
     translator = cli_translator.CLITranslator(
         mod_info=mod_info,
diff --git a/atest/module_info.py b/atest/module_info.py
index a364efe..308fa6b 100644
--- a/atest/module_info.py
+++ b/atest/module_info.py
@@ -29,7 +29,7 @@
 import time
 
 from pathlib import Path
-from typing import Any, Dict, List, Set
+from typing import Any, Callable, Dict, List, Set
 
 from atest import atest_utils
 from atest import constants
@@ -55,33 +55,41 @@
 def load_from_file(
         module_file: Path = None,
         force_build: bool = False,
+        save_timestamps: bool = False,
     ) -> ModuleInfo:
     """Factory method that initializes ModuleInfo from the build-generated
     JSON file"""
-    return ModuleInfo(module_file=module_file, force_build=force_build)
-
-
-def load_from_dict(name_to_module_info: Dict[str, Any]) -> ModuleInfo:
-    """Factory method that initializes ModuleInfo from a dictionary."""
-    with tempfile.NamedTemporaryFile(mode='w') as f:
-        # TODO: Serialize the input dict to JSON.
-        json.dump({}, f)
-        mi = load_from_file(module_file=f.name)
-
-    mi.loader.name_to_module_info.update(name_to_module_info)
+    mod_start = time.time()
+    loader = Loader(
+        module_file=module_file, force_build=force_build)
+    mi = loader.load(save_timestamps=save_timestamps)
+    mod_stop = time.time() - mod_start
+    metrics.LocalDetectEvent(
+        detect_type=DetectType.MODULE_INFO_INIT_MS, result=int(mod_stop * 1000))
 
     return mi
 
 
+def load_from_dict(name_to_module_info: Dict[str, Any]) -> ModuleInfo:
+    """Factory method that initializes ModuleInfo from a dictionary."""
+    path_to_module_info = get_path_to_module_info(name_to_module_info)
+    return ModuleInfo(
+        name_to_module_info=name_to_module_info,
+        path_to_module_info=path_to_module_info,
+        get_testable_modules=lambda s: _get_testable_modules(
+            name_to_module_info, path_to_module_info, s),
+    )
+
+
 def create_empty() -> ModuleInfo:
     """Factory method that initializes an empty ModuleInfo."""
-    return ModuleInfo(no_generate=True)
+    return ModuleInfo()
 
 
 class Loader:
     """Class that handles load and merge processes."""
 
-    def __init__(self, module_file=None, force_build: bool=False, no_generate: bool=False):
+    def __init__(self, module_file: Path=None, force_build: bool=False):
         self.java_dep_path = Path(
             atest_utils.get_build_out_dir()).joinpath('soong', _JAVA_DEP_INFO)
         self.cc_dep_path = Path(
@@ -99,12 +107,7 @@
         # module_info_target stays None.
         self.module_info_target = None
 
-        if no_generate:
-            self.name_to_module_info = {}
-            return
-
-        self.name_to_module_info, self.path_to_module_info = self.load(
-            module_file)
+        self.name_to_module_info, self.path_to_module_info = self._load_module_info_file()
 
         # Index and checksum files that will be used.
         index_dir = Path(os.getenv(constants.ANDROID_HOST_OUT, '')).joinpath('indexes')
@@ -119,11 +122,21 @@
                     func=self._get_testable_modules,
                     kwargs={'index': True})
 
-    def load(self, module_file):
-        """Load the module file.
+    def load(self, save_timestamps: bool=False):
+        if save_timestamps:
+            atest_utils.run_multi_proc(func=self._save_module_info_timestamp)
+            atest_utils.run_multi_proc(func=atest_utils.save_build_files_timestamp)
 
-        No matter whether passing module_file or not, ModuleInfo will load
-        atest_merged_dep.json as module info eventually.
+        return ModuleInfo(
+            name_to_module_info=self.name_to_module_info,
+            path_to_module_info=self.path_to_module_info,
+            module_info_target=self.module_info_target,
+            mod_info_file_path=self.mod_info_file_path,
+            get_testable_modules=self.get_testable_modules,
+        )
+
+    def _load_module_info_file(self):
+        """Load the module file as ModuleInfo.
 
         +--------------+                  +----------------------------------+
         | ModuleInfo() |                  | ModuleInfo(module_file=foo.json) |
@@ -144,15 +157,10 @@
         |    /atest_merged_dep.json  |--> load as module info.
         +============================+
 
-        Args:
-            module_file: String of path to file to load up. Used for testing.
-                         Note: if set, ModuleInfo will skip build process.
-
         Returns:
             Dict of module name to module info and dict of module path to module info.
         """
-        file_path = module_file
-        if not file_path:
+        if not self.mod_info_file_path:
             self.module_info_target, file_path = _discover_mod_file_and_target(
                 self.force_build)
             self.mod_info_file_path = Path(file_path)
@@ -344,29 +352,10 @@
         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)
+        modules = _get_testable_modules(
+            self.name_to_module_info, self.path_to_module_info, suite)
         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):
@@ -392,9 +381,12 @@
 
     def __init__(
         self,
-        force_build=False,
-        module_file=None,
-        no_generate=False):
+        name_to_module_info: Dict[str, Any]=None,
+        path_to_module_info: Dict[str, Any]=None,
+        module_info_target: str=None,
+        mod_info_file_path: Path=None,
+        get_testable_modules: Callable=None,
+        ):
         """Initialize the ModuleInfo object.
 
         Load up the module-info.json file and initialize the helper vars.
@@ -424,57 +416,20 @@
                 +============================+
 
         Args:
-            force_build: Boolean to indicate if we should rebuild the
-                         module_info file regardless if it's created or not.
-            module_file: String of path to file to load up. Used for testing.
-            no_generate: Boolean to indicate if we should populate module info
-                         from the soong artifacts; setting to true will
-                         leave module info empty.
+            name_to_module_info: Dict of name to module info.
+            path_to_module_info: Dict of path to module info.
+            module_info_target: Target name of module-info.json.
+            mod_info_file_path: Path of module-info.json.
+            get_testable_modules: Function to get all testable modules.
         """
         self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
         self.roboleaf_tests = {}
 
-        self.loader = Loader(
-            module_file=module_file,
-            force_build=force_build,
-            no_generate=no_generate,
-        )
-
-    @property
-    def name_to_module_info(self):
-        return self.loader.name_to_module_info
-
-    @property
-    def path_to_module_info(self):
-        return self.loader.path_to_module_info
-
-    @property
-    def module_info_target(self):
-        return self.loader.module_info_target
-
-    @property
-    def mod_info_file_path(self):
-        return self.loader.mod_info_file_path
-
-    @mod_info_file_path.setter
-    def mod_info_file_path(self, value):
-        self.loader.mod_info_file_path = value
-
-    @property
-    def java_dep_path(self):
-        return self.loader.java_dep_path
-
-    @property
-    def cc_dep_path(self):
-        return self.loader.cc_dep_path
-
-    @property
-    def merged_dep_path(self):
-        return self.loader.merged_dep_path
-
-    def _save_module_info_timestamp(self):
-        """Caller of the same method in Loader class."""
-        return self.loader._save_module_info_timestamp()
+        self.name_to_module_info = name_to_module_info or {}
+        self.path_to_module_info = path_to_module_info or {}
+        self.module_info_target = module_info_target
+        self.mod_info_file_path = mod_info_file_path
+        self._get_testable_modules = get_testable_modules
 
     def is_module(self, name):
         """Return True if name is a module, False otherwise."""
@@ -527,7 +482,7 @@
             constants.MODULE_COMPATIBILITY_SUITES, [])
 
     def get_testable_modules(self, suite=None):
-        return self.loader.get_testable_modules(suite)
+        return self._get_testable_modules(suite)
 
     @staticmethod
     def is_tradefed_testable_module(info: Dict[str, Any]) -> bool:
@@ -844,15 +799,6 @@
             return True
         return False
 
-    def _merge_build_system_infos(self, name_to_module_info,
-        java_bp_info_path=None, cc_bp_info_path=None):
-        """Caller of the same method in Loader class."""
-        return self.loader._merge_build_system_infos(
-            name_to_module_info,
-            java_bp_info_path,
-            cc_bp_info_path,
-        )
-
     def get_filepath_from_module(self, module_name: str, filename: str) -> Path:
         """Return absolute path of the given module and filename."""
         mod_path = self.get_paths(module_name)
@@ -1263,3 +1209,30 @@
     if _is_legacy_robolectric_test(name_to_module_info, path_to_module_info, info):
         return True
     return False
+
+def _get_testable_modules(
+    name_to_module_info: Dict[str, Dict],
+    path_to_module_info: Dict[str, Dict],
+    suite: bool=None):
+
+    modules = set()
+    begin = time.time()
+    for _, info in name_to_module_info.items():
+        if _is_testable_module(name_to_module_info, 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 suite:
+        _modules = set()
+        for module_name in modules:
+            info = 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
diff --git a/atest/module_info_unittest.py b/atest/module_info_unittest.py
index bc4a493..b8535f0 100755
--- a/atest/module_info_unittest.py
+++ b/atest/module_info_unittest.py
@@ -124,7 +124,7 @@
         default_out_dir_mod_targ = 'out/dir/here/module-info.json'
         # Make sure module_info_target is what we think it is.
         with mock.patch.dict('os.environ', os_environ_mock, clear=True):
-            mod_info = module_info.ModuleInfo()
+            mod_info = module_info.load_from_file()
             self.assertEqual(default_out_dir_mod_targ,
                              mod_info.module_info_target)
 
@@ -135,7 +135,7 @@
         custom_out_dir_mod_targ = 'out2/dir/here/module-info.json'
         # Make sure module_info_target is what we think it is.
         with mock.patch.dict('os.environ', os_environ_mock, clear=True):
-            mod_info = module_info.ModuleInfo()
+            mod_info = module_info.load_from_file()
             self.assertEqual(custom_out_dir_mod_targ,
                              mod_info.module_info_target)
 
@@ -146,7 +146,7 @@
         custom_abs_out_dir_mod_targ = '/tmp/out/dir/module-info.json'
         # Make sure module_info_target is what we think it is.
         with mock.patch.dict('os.environ', os_environ_mock, clear=True):
-            mod_info = module_info.ModuleInfo()
+            mod_info = module_info.load_from_file()
             self.assertEqual(custom_abs_out_dir_mod_targ,
                              mod_info.module_info_target)
 
@@ -279,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.loader.name_to_module_info = MOD_INFO_DICT
+        mod_info.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))
@@ -287,12 +287,12 @@
 
     def test_merge_build_system_infos(self):
         """Test _merge_build_system_infos."""
-        mod_info = module_info.load_from_file(module_file=JSON_FILE_PATH)
+        loader = module_info.Loader(module_file=JSON_FILE_PATH)
         mod_info_1 = {constants.MODULE_NAME: 'module_1',
                       constants.MODULE_DEPENDENCIES: []}
         name_to_mod_info = {'module_1' : mod_info_1}
         expect_deps = ['test_dep_level_1_1', 'test_dep_level_1_2']
-        name_to_mod_info = mod_info._merge_build_system_infos(
+        name_to_mod_info = loader._merge_build_system_infos(
             name_to_mod_info, java_bp_info_path=self.java_dep_path)
         self.assertEqual(
             name_to_mod_info['module_1'].get(constants.MODULE_DEPENDENCIES),
@@ -300,8 +300,8 @@
 
     def test_merge_build_system_infos_missing_keys(self):
         """Test _merge_build_system_infos for keys missing from module-info.json."""
-        mod_info = module_info.load_from_file(module_file=JSON_FILE_PATH)
-        name_to_mod_info = mod_info._merge_build_system_infos(
+        loader = module_info.Loader(module_file=JSON_FILE_PATH)
+        name_to_mod_info = loader._merge_build_system_infos(
             {}, java_bp_info_path=self.java_dep_path)
 
         expect_deps = ['test_dep_level_1_1']
@@ -311,12 +311,12 @@
 
     def test_merge_dependency_with_ori_dependency(self):
         """Test _merge_dependency."""
-        mod_info = module_info.load_from_file(module_file=JSON_FILE_PATH)
+        loader = module_info.Loader(module_file=JSON_FILE_PATH)
         mod_info_1 = {constants.MODULE_NAME: 'module_1',
                       constants.MODULE_DEPENDENCIES: ['ori_dep_1']}
         name_to_mod_info = {'module_1' : mod_info_1}
         expect_deps = ['ori_dep_1', 'test_dep_level_1_1', 'test_dep_level_1_2']
-        name_to_mod_info = mod_info._merge_build_system_infos(
+        name_to_mod_info = loader._merge_build_system_infos(
             name_to_mod_info, java_bp_info_path=self.java_dep_path)
         self.assertEqual(
             name_to_mod_info['module_1'].get(constants.MODULE_DEPENDENCIES),
@@ -391,34 +391,37 @@
 
     def test_get_module_dependency(self):
         """Test get_module_dependency."""
-        mod_info = module_info.load_from_file(module_file=JSON_FILE_PATH)
+        loader = module_info.Loader(module_file=JSON_FILE_PATH)
+        mod_info = loader.load()
         expect_deps = {'test_dep_level_1_1', 'module_1', 'test_dep_level_1_2',
                        'test_dep_level_2_2', 'test_dep_level_2_1', 'module_2'}
-        mod_info._merge_build_system_infos(mod_info.name_to_module_info,
-                                   java_bp_info_path=self.java_dep_path)
+        loader._merge_build_system_infos(
+            loader.name_to_module_info, java_bp_info_path=self.java_dep_path)
         self.assertEqual(
             mod_info.get_module_dependency('dep_test_module'),
             expect_deps)
 
     def test_get_module_dependency_w_loop(self):
         """Test get_module_dependency with problem dep file."""
-        mod_info = module_info.load_from_file(module_file=JSON_FILE_PATH)
+        loader = module_info.Loader(module_file=JSON_FILE_PATH)
+        mod_info = loader.load()
         # Java dependency file with a endless loop define.
         java_dep_file = os.path.join(uc.TEST_DATA_DIR,
                                      'module_bp_java_loop_deps.json')
         expect_deps = {'test_dep_level_1_1', 'module_1', 'test_dep_level_1_2',
                        'test_dep_level_2_2', 'test_dep_level_2_1', 'module_2'}
-        mod_info._merge_build_system_infos(mod_info.name_to_module_info,
-                                   java_bp_info_path=java_dep_file)
+        loader._merge_build_system_infos(
+            loader.name_to_module_info, java_bp_info_path=java_dep_file)
         self.assertEqual(
             mod_info.get_module_dependency('dep_test_module'),
             expect_deps)
 
     def test_get_install_module_dependency(self):
         """Test get_install_module_dependency."""
-        mod_info = module_info.load_from_file(module_file=JSON_FILE_PATH)
+        loader = module_info.Loader(module_file=JSON_FILE_PATH)
+        mod_info = loader.load()
         expect_deps = {'module_1', 'test_dep_level_2_1'}
-        mod_info._merge_build_system_infos(mod_info.name_to_module_info,
+        loader._merge_build_system_infos(loader.name_to_module_info,
                                            java_bp_info_path=self.java_dep_path)
         self.assertEqual(
             mod_info.get_install_module_dependency('dep_test_module'),
@@ -426,12 +429,12 @@
 
     def test_cc_merge_build_system_infos(self):
         """Test _merge_build_system_infos for cc."""
-        mod_info = module_info.load_from_file(module_file=JSON_FILE_PATH)
+        loader = module_info.Loader(module_file=JSON_FILE_PATH)
         mod_info_1 = {constants.MODULE_NAME: 'module_cc_1',
                       constants.MODULE_DEPENDENCIES: []}
         name_to_mod_info = {'module_cc_1' : mod_info_1}
         expect_deps = ['test_cc_dep_level_1_1', 'test_cc_dep_level_1_2']
-        name_to_mod_info = mod_info._merge_build_system_infos(
+        name_to_mod_info = loader._merge_build_system_infos(
             name_to_mod_info, cc_bp_info_path=self.cc_dep_path)
         self.assertEqual(
             name_to_mod_info['module_cc_1'].get(constants.MODULE_DEPENDENCIES),
@@ -660,7 +663,7 @@
         modules = modules or []
 
         for m in modules:
-            mod_info.loader.name_to_module_info[m['module_name']] = m
+            mod_info.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 20fea6e..787ff89 100755
--- a/atest/test_finders/module_finder_unittest.py
+++ b/atest/test_finders/module_finder_unittest.py
@@ -212,18 +212,13 @@
         return module_info.load_from_file(module_file=fake_temp_file_name)
 
     def create_module_info(self, modules=None):
-        mod_info = self.create_empty_module_info()
         modules = modules or []
+        name_to_module_info = {}
 
         for m in modules:
-            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)
-                else:
-                    mod_info.path_to_module_info[path] = [m]
+            name_to_module_info[m['module_name']] = m
 
-        return mod_info
+        return module_info.load_from_dict(name_to_module_info=name_to_module_info)
 
     # TODO: remove below mocks and hide unnecessary information.
     @mock.patch.object(module_finder.ModuleFinder, '_get_test_info_filter')
@@ -1383,13 +1378,13 @@
 
 
 def create_module_info(modules=None):
-    mod_info = create_empty_module_info()
+    name_to_module_info = {}
     modules = modules or []
 
     for m in modules:
-        mod_info.loader.name_to_module_info[m['module_name']] = m
+        name_to_module_info[m['module_name']] = m
 
-    return mod_info
+    return module_info.load_from_dict(name_to_module_info)
 
 
 # pylint: disable=too-many-arguments
@@ -1414,7 +1409,7 @@
 
     m['module_name'] = name
     m['class'] = classes
-    m['path'] = [path or '']
+    m['path'] = path or ['']
     m['installed'] = installed or []
     m['is_unit_test'] = 'false'
     m['auto_test_config'] = auto_test_config or []
diff --git a/atest/test_runner_handler_unittest.py b/atest/test_runner_handler_unittest.py
index 6686bbf..c868461 100755
--- a/atest/test_runner_handler_unittest.py
+++ b/atest/test_runner_handler_unittest.py
@@ -129,7 +129,7 @@
         """Test that the return value as we expected."""
         results_dir = ""
         extra_args = {}
-        mod_info = module_info.ModuleInfo(
+        mod_info = module_info.load_from_file(
             module_file=os.path.join(uc.TEST_DATA_DIR, uc.JSON_FILE))
         # Tests both run_tests return 0
         test_infos = [MODULE_INFO_A, MODULE_INFO_A_AGAIN]