Merge "AIDEGen: Refactor project_info.py for using in class MainProjectInfo."
diff --git a/aidegen/aidegen_main.py b/aidegen/aidegen_main.py
index 0eb4530..168b787 100644
--- a/aidegen/aidegen_main.py
+++ b/aidegen/aidegen_main.py
@@ -352,6 +352,30 @@
 
 
 @common_util.back_to_cwd
+def _adjust_cwd_for_whole_tree_project(args, mod_info):
+    """The wrapper to handle the directory change for whole tree case.
+
+        Args:
+            args: A list of system arguments.
+            mod_info: A instance of atest module_info.
+
+        Returns:
+            A list of ProjectInfo instance
+
+    """
+    targets = _check_whole_android_tree(
+        mod_info, args.targets, args.android_tree)
+    project_info.ProjectInfo.modules_info = module_info.AidegenModuleInfo(
+        force_build=False,
+        module_file=None,
+        atest_module_info=mod_info,
+        projects=targets,
+        verbose=args.verbose,
+        skip_build=args.skip_build)
+
+    return project_info.ProjectInfo.generate_projects(targets)
+
+
 def aidegen_main(args):
     """AIDEGen main entry.
 
@@ -364,16 +388,7 @@
     ide_util_obj = _get_ide_util_instance(args)
     project_info.ProjectInfo.config = project_config.ProjectConfig(args)
     atest_module_info = common_util.get_atest_module_info(args.targets)
-    targets = _check_whole_android_tree(
-        atest_module_info, args.targets, args.android_tree)
-    project_info.ProjectInfo.modules_info = module_info.AidegenModuleInfo(
-        force_build=False,
-        module_file=None,
-        atest_module_info=atest_module_info,
-        projects=targets,
-        verbose=args.verbose,
-        skip_build=args.skip_build)
-    projects = project_info.ProjectInfo.generate_projects(targets)
+    projects = _adjust_cwd_for_whole_tree_project(args, atest_module_info)
     source_locator.multi_projects_locate_source(projects, args.verbose)
     _generate_project_files(projects)
     if ide_util_obj:
diff --git a/aidegen/aidegen_main_unittest.py b/aidegen/aidegen_main_unittest.py
index fc6923c..eff2869 100644
--- a/aidegen/aidegen_main_unittest.py
+++ b/aidegen/aidegen_main_unittest.py
@@ -69,13 +69,15 @@
         args = aidegen_main._parse_args(['-s'])
         self.assertEqual(args.skip_build, True)
 
+    @mock.patch.object(ide_util.IdeIntelliJ, '_get_preferred_version')
     @mock.patch.object(ide_util.IdeUtil, 'is_ide_installed')
-    def test_get_ide_util_instance(self, mock_installed):
+    def test_get_ide_util_instance(self, mock_installed, mock_preference):
         """Test _get_ide_util_instance with different conditions."""
         target = 'tradefed'
         args = aidegen_main._parse_args([target, '-n'])
         self.assertEqual(aidegen_main._get_ide_util_instance(args), None)
         args = aidegen_main._parse_args([target])
+        mock_preference.return_value = None
         self.assertIsInstance(
             aidegen_main._get_ide_util_instance(args), ide_util.IdeUtil)
         mock_installed.return_value = False
diff --git a/aidegen/lib/aidegen_metrics_unittest.py b/aidegen/lib/aidegen_metrics_unittest.py
index d228956..b4c037a 100644
--- a/aidegen/lib/aidegen_metrics_unittest.py
+++ b/aidegen/lib/aidegen_metrics_unittest.py
@@ -35,28 +35,28 @@
 class AidegenMetricsUnittests(unittest.TestCase):
     """Unit tests for aidegen_metrics.py."""
 
-    @mock.patch.object(metrics, 'AtestStartEvent')
-    @mock.patch.object(metrics_utils, 'get_start_time')
     @mock.patch.object(atest_utils, 'print_data_collection_notice')
-    def test_starts_asuite_metrics(self, mock_print_data, mock_get_start_time,
-                                   mock_start_event):
+    def test_starts_asuite_metrics(self, mock_print_data):
         """Test starts_asuite_metrics."""
         references = ['nothing']
-        aidegen_metrics.starts_asuite_metrics(references)
         if not metrics:
+            aidegen_metrics.starts_asuite_metrics(references)
             self.assertFalse(mock_print_data.called)
         else:
-            self.assertTrue(mock_print_data.called)
-            self.assertTrue(mock_get_start_time.called)
-            self.assertTrue(mock_start_event.called)
+            with mock.patch.object(metrics_utils, 'get_start_time') as mk_get:
+                with mock.patch.object(metrics, 'AtestStartEvent') as mk_start:
+                    aidegen_metrics.starts_asuite_metrics(references)
+                    self.assertTrue(mock_print_data.called)
+                    self.assertTrue(mk_get.called)
+                    self.assertTrue(mk_start.called)
 
-    @mock.patch.object(metrics_utils, 'send_exit_event')
-    def test_ends_asuite_metrics(self, mock_send_exit_event):
+    def test_ends_asuite_metrics(self):
         """Test ends_asuite_metrics."""
         exit_code = constant.EXIT_CODE_NORMAL
-        aidegen_metrics.ends_asuite_metrics(exit_code)
         if metrics_utils:
-            self.assertTrue(mock_send_exit_event.called)
+            with mock.patch.object(metrics_utils, 'send_exit_event') as mk_send:
+                aidegen_metrics.ends_asuite_metrics(exit_code)
+                self.assertTrue(mk_send.called)
 
 
 if __name__ == '__main__':
diff --git a/aidegen/lib/common_util.py b/aidegen/lib/common_util.py
index 87a7c6c..a3de970 100644
--- a/aidegen/lib/common_util.py
+++ b/aidegen/lib/common_util.py
@@ -287,9 +287,11 @@
     """Determine if the given project path is relative to the module.
 
     The rules:
-       1. If project_relative_path is empty, it's under Android root, return
+       1. If constant.KEY_PATH not in data, we can't tell if it's a module
+          return False.
+       2. If project_relative_path is empty, it's under Android root, return
           True.
-       2. If module's path equals or starts with project_relative_path return
+       3. If module's path equals or starts with project_relative_path return
           True, otherwise return False.
 
     Args:
@@ -300,12 +302,12 @@
         True if it's the given project path is relative to the module, otherwise
         False.
     """
-    if 'path' not in data:
+    if constant.KEY_PATH not in data:
         return False
-    path = data['path'][0]
+    path = data[constant.KEY_PATH][0]
     if project_relative_path == '':
         return True
-    if ('class' in data
+    if (constant.KEY_CLASS in data
             and (path == project_relative_path
                  or path.startswith(project_relative_path + os.sep))):
         return True
@@ -415,7 +417,7 @@
         Android out directory path.
     """
     android_root_path = get_android_root_dir()
-    android_out_dir = os.environ.get(constants.ANDROID_OUT_DIR)
+    android_out_dir = os.getenv(constants.ANDROID_OUT_DIR)
     out_dir_common_base = os.getenv(constant.OUT_DIR_COMMON_BASE_ENV_VAR)
     android_out_dir_common_base = (os.path.join(
         out_dir_common_base, os.path.basename(android_root_path))
diff --git a/aidegen/lib/common_util_unittest.py b/aidegen/lib/common_util_unittest.py
index 01df761..d59ff34 100644
--- a/aidegen/lib/common_util_unittest.py
+++ b/aidegen/lib/common_util_unittest.py
@@ -164,6 +164,65 @@
             mock_log_config.called_with(
                 level=level, format=log_format, datefmt=datefmt))
 
+    def test_is_project_path_relative_module(self):
+        """Test is_project_path_relative_module handling."""
+        data = {'class':['APPS']}
+        self.assertFalse(common_util.is_project_path_relative_module(data, ''))
+        data = {'class':['APPS'], 'path':['path_to_a']}
+        self.assertTrue(common_util.is_project_path_relative_module(data, ''))
+        self.assertFalse(
+            common_util.is_project_path_relative_module(data, 'test'))
+        data = {'path':['path_to_a']}
+        self.assertFalse(
+            common_util.is_project_path_relative_module(data, 'test'))
+        self.assertFalse(
+            common_util.is_project_path_relative_module(data, 'path_to_a'))
+        data = {'class':['APPS'], 'path':['test/path_to_a']}
+        self.assertTrue(
+            common_util.is_project_path_relative_module(data, 'test'))
+        self.assertFalse(
+            common_util.is_project_path_relative_module(data, 'tes'))
+
+    @mock.patch.object(common_util, '_check_modules')
+    @mock.patch.object(module_info, 'ModuleInfo')
+    def test_get_atest_module_info(self, mock_modinfo, mock_check_modules):
+        """Test get_atest_module_info handling."""
+        common_util.get_atest_module_info()
+        self.assertEqual(mock_modinfo.call_count, 1)
+        mock_modinfo.reset_mock()
+        mock_check_modules.return_value = False
+        common_util.get_atest_module_info(['nothing'])
+        self.assertEqual(mock_modinfo.call_count, 2)
+
+    @mock.patch('builtins.open', create=True)
+    def test_read_file_content(self, mock_open):
+        """Test read_file_content handling."""
+        expacted_data1 = 'Data1'
+        fileA = 'fileA'
+        mock_open.side_effect = [
+            mock.mock_open(read_data=expacted_data1).return_value
+        ]
+        self.assertEqual(expacted_data1, common_util.read_file_content(fileA))
+        mock_open.assert_called_once_with(fileA)
+
+    @mock.patch('os.getenv')
+    @mock.patch.object(common_util, 'get_android_root_dir')
+    def test_get_android_out_dir(self, mock_get_android_root_dir, mock_getenv):
+        """Test get_android_out_dir handling."""
+        root = 'my/path-to-root/master'
+        default_root = 'out'
+        android_out_root = 'eng_out'
+        mock_get_android_root_dir.return_value = root
+        mock_getenv.side_effect = ['', '']
+        self.assertEqual(default_root, common_util.get_android_out_dir())
+        mock_getenv.side_effect = [android_out_root, '']
+        self.assertEqual(android_out_root, common_util.get_android_out_dir())
+        mock_getenv.side_effect = ['', default_root]
+        self.assertEqual(os.path.join(default_root, os.path.basename(root)),
+                         common_util.get_android_out_dir())
+        mock_getenv.side_effect = [android_out_root, default_root]
+        self.assertEqual(android_out_root, common_util.get_android_out_dir())
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/aidegen/lib/ide_util_unittest.py b/aidegen/lib/ide_util_unittest.py
index 597aab0..ff0f073 100644
--- a/aidegen/lib/ide_util_unittest.py
+++ b/aidegen/lib/ide_util_unittest.py
@@ -32,6 +32,7 @@
 
 
 # pylint: disable=protected-access
+# pylint: disable-msg=too-many-arguments
 class IdeUtilUnittests(unittest.TestCase):
     """Unit tests for ide_util.py."""
 
@@ -118,8 +119,10 @@
         ide_util._get_ide(None, 'j', False, is_mac=False)
         self.assertTrue(mock_linux.called)
 
-    def test_get_mac_and_linux_ide(self):
+    @mock.patch.object(ide_util.IdeIntelliJ, '_get_preferred_version')
+    def test_get_mac_and_linux_ide(self, mock_preference):
         """Test if _get_mac_ide and _get_linux_ide return correct IDE class."""
+        mock_preference.return_value = None
         self.assertIsInstance(ide_util._get_mac_ide(), ide_util.IdeMacIntelliJ)
         self.assertIsInstance(ide_util._get_mac_ide(None, 's'),
                               ide_util.IdeMacStudio)
@@ -145,13 +148,16 @@
         ide_util.IdeMacIntelliJ('some_path')
         self.assertTrue(mock_input.called)
 
+    @mock.patch.object(ide_util.IdeIntelliJ, '_get_preferred_version')
     @mock.patch.object(sdk_config.SDKConfig, '_android_sdk_exists')
     @mock.patch.object(sdk_config.SDKConfig, '_target_jdk_exists')
     @mock.patch.object(ide_util.IdeIntelliJ, '_get_config_root_paths')
     @mock.patch.object(ide_util.IdeBase, 'apply_optional_config')
-    def test_config_ide(self, mock_config, mock_paths, mock_jdk, mock_sdk):
+    def test_config_ide(self, mock_config, mock_paths, mock_jdk, mock_sdk,
+                        mock_preference):
         """Test IDEA, IdeUtil.config_ide won't call base none implement api."""
         # Mock SDkConfig flow to not to generate real jdk config file.
+        mock_preference.return_value = None
         mock_jdk.return_value = True
         mock_sdk.return_value = True
         test_path = os.path.join(tempfile.mkdtemp())
@@ -173,9 +179,9 @@
         """Test to get unique config path for linux IDEA case."""
         if (not android_dev_os.AndroidDevOS.MAC ==
                 android_dev_os.AndroidDevOS.get_os_type()):
-            mock_path.return_value = '/opt/intelliJ-ce-2018.3/bin/idea.sh'
-            mock_path_2.return_value = '/opt/intelliJ-ce-2018.3/bin/idea.sh'
-            ide_obj = ide_util.IdeLinuxIntelliJ()
+            mock_path.return_value = '/opt/intellij-ce-2018.3/bin/idea.sh'
+            mock_path_2.return_value = '/opt/intellij-ce-2018.3/bin/idea.sh'
+            ide_obj = ide_util.IdeLinuxIntelliJ('default_path')
             self.assertEqual(1, len(ide_obj._get_config_root_paths()))
         else:
             self.assertTrue((android_dev_os.AndroidDevOS.MAC ==
@@ -244,7 +250,7 @@
         original_path = 'intellij-ce-2019.1/bin/idea.sh'
         mock_isfile.return_value = True
         mock_realpath.return_value = original_path
-        ide_obj = ide_util.IdeLinuxIntelliJ()
+        ide_obj = ide_util.IdeLinuxIntelliJ('default_path')
         merged_version = ide_obj._merge_symbolic_version(
             [symbolic_path, original_path])
         self.assertEqual(
diff --git a/aidegen/lib/module_info_util.py b/aidegen/lib/module_info_util.py
index 794afa7..47fd8fd 100644
--- a/aidegen/lib/module_info_util.py
+++ b/aidegen/lib/module_info_util.py
@@ -29,13 +29,15 @@
 import json
 import logging
 import os
-import subprocess
 import sys
 
 from aidegen import constant
 from aidegen.lib import common_util
 from aidegen.lib import errors
 
+from atest import atest_utils
+from atest import constants
+
 _BLUEPRINT_JSONFILE_NAME = 'module_bp_java_deps.json'
 _MERGE_NEEDED_ITEMS = [
     constant.KEY_CLASS,
@@ -55,8 +57,9 @@
 _LAUNCH_PROJECT_QUERY = (
     'There exists an IntelliJ project file: %s. Do you want '
     'to launch it (yes/No)?')
-_GENERATE_JSON_COMMAND = ('SOONG_COLLECT_JAVA_DEPS=false make nothing -C {DIR};'
-                          'SOONG_COLLECT_JAVA_DEPS=true make nothing -C {DIR}')
+
+_BUILD_BP_JSON_ENV_OFF = {'SOONG_COLLECT_JAVA_DEPS': 'false'}
+_BUILD_BP_JSON_ENV_ON = constants.ATEST_BUILD_ENV
 
 
 @common_util.time_logged
@@ -84,32 +87,29 @@
     Returns:
         A merged dictionary from module-info.json and module_bp_java_deps.json.
     """
-    main_project = projects[0] if projects else None
-    cmd = [_GENERATE_JSON_COMMAND.format(
-        DIR=common_util.get_android_root_dir())]
-    _build_target(module_info, cmd, main_project, verbose, skip_build)
+    json_path = common_util.get_blueprint_json_path()
+    if not os.path.isfile(json_path):
+        main_project = projects[0] if projects else None
+        _build_bp_info(module_info, main_project, verbose, skip_build)
     bp_dict = _get_soong_build_json_dict()
     return _merge_dict(module_info.name_to_module_info, bp_dict)
 
 
-def _build_target(module_info, cmd, main_project=None, verbose=False,
-                  skip_build=False):
+def _build_bp_info(module_info, main_project=None, verbose=False,
+                   skip_build=False):
     """Make nothing to generate module_bp_java_deps.json.
 
-    We build without environment setting SOONG_COLLECT_JAVA_DEPS and then build
-    with environment setting SOONG_COLLECT_JAVA_DEPS. In this way we can trigger
-    the process of collecting dependencies and generating
-    module_bp_java_deps.json.
+    Using atest build method with set env config SOONG_COLLECT_JAVA_DEPS=true to
+    build the target nothing. By this way to trigger the process of collecting
+    dependencies and generating module_bp_java_deps.json.
 
     Args:
         module_info: A ModuleInfo instance contains data of module-info.json.
-        cmd: A string list, build command.
         main_project: The main project name.
         verbose: A boolean, if true displays full build output.
         skip_build: A boolean, if true, skip building if
                     get_blueprint_json_path() file exists, otherwise
                     build it.
-
     Build results:
         1. Build successfully return.
         2. Build failed:
@@ -127,15 +127,18 @@
                          common_util.get_blueprint_json_path())
             return
         original_json_mtime = os.path.getmtime(json_path)
-    try:
-        if verbose:
-            full_env_vars = os.environ.copy()
-            subprocess.check_call(
-                cmd, stderr=subprocess.STDOUT, env=full_env_vars, shell=True)
-        else:
-            subprocess.check_call(cmd, shell=True)
-        logging.info('Build successfully: %s.', cmd)
-    except subprocess.CalledProcessError:
+
+    logging.warning('\nUse atest build method to generate blueprint json.')
+    # Force build system to always generate the blueprint json file by setting
+    # SOONG_COLLECT_JAVA_DEPS to false, then true.
+    build_with_off_cmd = atest_utils.build(['nothing'], verbose,
+                                           _BUILD_BP_JSON_ENV_OFF)
+    build_with_on_cmd = atest_utils.build(['nothing'], verbose,
+                                          _BUILD_BP_JSON_ENV_ON)
+
+    if build_with_off_cmd and build_with_on_cmd:
+        logging.info('\nGenerate blueprint json successfully.')
+    else:
         if not _is_new_json_file_generated(json_path, original_json_mtime):
             if os.path.isfile(json_path):
                 message = ('Generate new {0} failed, AIDEGen will proceed and '
@@ -180,7 +183,7 @@
     project_file = glob.glob(
         os.path.join(main_project_path, _INTELLIJ_PROJECT_FILE_EXT))
     if project_file:
-        query = (_LAUNCH_PROJECT_QUERY) % project_file[0]
+        query = _LAUNCH_PROJECT_QUERY % project_file[0]
         input_data = input(query)
         if not input_data.lower() in ['yes', 'y']:
             sys.exit(1)
@@ -251,7 +254,7 @@
     """
     merged_dict = _copy_needed_items_from(mk_dict)
     for module in bp_dict.keys():
-        if not module in merged_dict.keys():
+        if module not in merged_dict.keys():
             merged_dict[module] = dict()
         _merge_module_keys(merged_dict[module], bp_dict[module])
     return merged_dict
diff --git a/aidegen/lib/module_info_util_unittest.py b/aidegen/lib/module_info_util_unittest.py
index c604968..24260e1 100644
--- a/aidegen/lib/module_info_util_unittest.py
+++ b/aidegen/lib/module_info_util_unittest.py
@@ -17,16 +17,17 @@
 """Unittests for module_info_utils."""
 
 import copy
-import os
-import subprocess
+import os.path
 import unittest
 from unittest import mock
 
 from aidegen import unittest_constants
+from aidegen.lib import common_util
 from aidegen.lib import errors
 from aidegen.lib import module_info_util
-from atest import module_info
 
+from atest import atest_utils
+from atest import module_info
 
 _TEST_CLASS_DICT = {'class': ['JAVA_LIBRARIES']}
 _TEST_SRCS_BAR_DICT = {'srcs': ['Bar']}
@@ -130,26 +131,24 @@
             module_info_util._copy_needed_items_from(
                 _TEST_MODULE_A_DICT_HAS_NONEED_ITEMS))
 
-    @mock.patch('subprocess.check_call')
-    @mock.patch('os.environ.copy')
-    def test_build_target_normal(self, mock_copy, mock_check_call):
+    @mock.patch.object(os.path, 'getmtime')
+    @mock.patch.object(atest_utils, 'build')
+    @mock.patch('os.path.isfile')
+    def test_build_bp_info_normal(self, mock_isfile, mock_build, mock_time):
         """Test _build_target with verbose true and false."""
-        mock_copy.return_value = ''
+        mock_isfile.return_value = True
         amodule_info = module_info.ModuleInfo()
-        cmd = [module_info_util._GENERATE_JSON_COMMAND]
-        module_info_util._build_target(amodule_info, cmd,
-                                       unittest_constants.TEST_MODULE, True)
-        self.assertTrue(mock_copy.called)
-        self.assertTrue(mock_check_call.called)
-        mock_check_call.assert_called_with(
-            cmd,
-            stderr=subprocess.STDOUT,
-            env=mock_copy.return_value,
-            shell=True)
-        module_info_util._build_target(amodule_info, cmd,
-                                       unittest_constants.TEST_MODULE, False)
-        self.assertTrue(mock_check_call.called)
-        mock_check_call.assert_called_with(cmd, shell=True)
+        skip = True
+        mock_build.return_value = True
+        module_info_util._build_bp_info(amodule_info, unittest_constants.
+                                        TEST_MODULE, False, skip)
+        self.assertFalse(mock_build.called)
+        skip = False
+        mock_time.return_value = float()
+        module_info_util._build_bp_info(amodule_info, unittest_constants.
+                                        TEST_MODULE, False, skip)
+        self.assertTrue(mock_time.called)
+        self.assertEqual(mock_build.call_count, 2)
 
     @mock.patch('os.path.getmtime')
     @mock.patch('os.path.isfile')
@@ -199,30 +198,24 @@
             module_info_util._get_soong_build_json_dict()
 
     @mock.patch('aidegen.lib.module_info_util._build_failed_handle')
+    @mock.patch.object(common_util, 'get_related_paths')
     @mock.patch('aidegen.lib.module_info_util._is_new_json_file_generated')
-    @mock.patch('subprocess.check_call')
-    def test_build_target(self, mock_call, mock_new, mock_handle):
-        """Test _build_target with different arguments."""
-        cmd = [module_info_util._GENERATE_JSON_COMMAND]
-        main_project = ''
+    @mock.patch.object(atest_utils, 'build')
+    def test_build_bp_info(self, mock_build, mock_new, mock_path, mock_handle):
+        """Test _build_bp_info with different arguments."""
+        main_project = 'Settings'
         amodule_info = {}
         verbose = False
-        module_info_util._build_target(amodule_info, cmd, main_project, verbose)
-        mock_call.assert_called_with(cmd, shell=True)
-        verbose = True
-        full_env_vars = os.environ.copy()
-        module_info_util._build_target(amodule_info, cmd, main_project, verbose)
-        mock_call.assert_called_with(cmd, stderr=subprocess.STDOUT,
-                                     env=full_env_vars, shell=True)
-        mock_call.side_effect = subprocess.CalledProcessError(1, '')
+        mock_build.return_value = False
         mock_new.return_value = False
-        module_info_util._build_target(amodule_info, cmd, main_project, verbose)
+        module_info_util._build_bp_info(amodule_info, main_project, verbose)
         self.assertTrue(mock_new.called)
         self.assertFalse(mock_handle.called)
         mock_new.return_value = True
-        module_info_util._build_target(amodule_info, cmd, main_project, verbose)
+        mock_path.return_value = None, 'packages/apps/Settings'
+        module_info_util._build_bp_info(amodule_info, main_project, verbose)
         self.assertTrue(mock_new.called)
-        self.assertFalse(mock_handle.called)
+        self.assertTrue(mock_handle.called)
 
 
 if __name__ == '__main__':