Merge "Include cluster client in log line for command cancellation."
diff --git a/atest/atest.py b/atest/atest.py
index 8611e21..9271a32 100755
--- a/atest/atest.py
+++ b/atest/atest.py
@@ -563,17 +563,17 @@
 # pylint: disable=too-many-statements
 # pylint: disable=too-many-branches
 # pylint: disable=too-many-return-statements
-def main(argv, results_dir):
+def main(argv, results_dir, args):
     """Entry point of atest script.
 
     Args:
         argv: A list of arguments.
         results_dir: A directory which stores the ATest execution information.
+        args: An argspace.Namespace class instance holding parsed args.
 
     Returns:
         Exit code.
     """
-    args = _parse_args(argv)
     _configure_logging(args.verbose)
     _validate_args(args)
     metrics_utils.get_start_time()
@@ -602,7 +602,8 @@
     mod_info = module_info.ModuleInfo(force_build=args.rebuild_module_info)
     if args.rebuild_module_info:
         _run_extra_tasks(join=True)
-    translator = cli_translator.CLITranslator(module_info=mod_info)
+    translator = cli_translator.CLITranslator(module_info=mod_info,
+                                              print_cache_msg=not args.clear_cache)
     if args.list_modules:
         _print_testable_modules(mod_info, args.list_modules)
         return constants.EXIT_CODE_SUCCESS
@@ -707,14 +708,16 @@
 
 if __name__ == '__main__':
     RESULTS_DIR = make_test_run_dir()
+    ARGS = _parse_args(sys.argv[1:])
     with atest_execution_info.AtestExecutionInfo(sys.argv[1:],
-                                                 RESULTS_DIR) as result_file:
+                                                 RESULTS_DIR,
+                                                 ARGS) as result_file:
         metrics_base.MetricsBase.tool_name = constants.TOOL_NAME
-        EXIT_CODE = main(sys.argv[1:], RESULTS_DIR)
+        EXIT_CODE = main(sys.argv[1:], RESULTS_DIR, ARGS)
         DETECTOR = bug_detector.BugDetector(sys.argv[1:], EXIT_CODE)
         metrics.LocalDetectEvent(
             detect_type=constants.DETECT_TYPE_BUG_DETECTED,
             result=DETECTOR.caught_result)
         if result_file:
-            print('Execution detail has saved in %s' % result_file.name)
+            print("Run 'atest --history' to review test result history.")
     sys.exit(EXIT_CODE)
diff --git a/atest/atest_execution_info.py b/atest/atest_execution_info.py
index a92fe1f..39cd3c1 100644
--- a/atest/atest_execution_info.py
+++ b/atest/atest_execution_info.py
@@ -100,6 +100,25 @@
                 pass
 
 
+def has_non_test_options(args):
+    """
+    check whether non-test option in the args.
+
+    Args:
+        args: An argspace.Namespace class instance holding parsed args.
+
+    Returns:
+        True, if args has at least one non-test option.
+        False, otherwise.
+    """
+    return (args.collect_tests_only
+            or args.dry_run
+            or args.help
+            or args.history
+            or args.info
+            or args.version)
+
+
 class AtestExecutionInfo(object):
     """Class that stores the whole test progress information in JSON format.
 
@@ -139,12 +158,13 @@
 
     result_reporters = []
 
-    def __init__(self, args, work_dir):
+    def __init__(self, args, work_dir, args_ns):
         """Initialise an AtestExecutionInfo instance.
 
         Args:
             args: Command line parameters.
-            work_dir : The directory for saving information.
+            work_dir: The directory for saving information.
+            args_ns: An argspace.Namespace class instance holding parsed args.
 
         Returns:
                A json format string.
@@ -152,6 +172,7 @@
         self.args = args
         self.work_dir = work_dir
         self.result_file = None
+        self.args_ns = args_ns
 
     def __enter__(self):
         """Create and return information file object."""
@@ -168,7 +189,8 @@
             self.result_file.write(AtestExecutionInfo.
                                    _generate_execution_detail(self.args))
             self.result_file.close()
-            symlink_latest_result(self.work_dir)
+            if not has_non_test_options(self.args_ns):
+                symlink_latest_result(self.work_dir)
         main_module = sys.modules.get(_MAIN_MODULE_KEY)
         main_exit_code = getattr(main_module, _EXIT_CODE_ATTR,
                                  constants.EXIT_CODE_ERROR)
diff --git a/atest/atest_utils.py b/atest/atest_utils.py
index 9a7178e..f1be007 100644
--- a/atest/atest_utils.py
+++ b/atest/atest_utils.py
@@ -16,6 +16,7 @@
 Utility functions for atest.
 """
 
+
 from __future__ import print_function
 
 import hashlib
@@ -36,13 +37,6 @@
 from metrics import metrics_base
 from metrics import metrics_utils
 
-try:
-    # If PYTHON2
-    from urllib2 import urlopen
-except ImportError:
-    metrics_utils.handle_exc_and_send_exit_event(
-        constants.IMPORT_FAILURE)
-    from urllib.request import urlopen
 
 _BASH_RESET_CODE = '\033[0m\n'
 # Arbitrary number to limit stdout for failed runs in _run_limited_output.
@@ -204,6 +198,13 @@
     # TODO: Also check if we have a slow connection to result server.
     if constants.RESULT_SERVER:
         try:
+            try:
+                # If PYTHON2
+                from urllib2 import urlopen
+            except ImportError:
+                metrics_utils.handle_exc_and_send_exit_event(
+                    constants.IMPORT_FAILURE)
+                from urllib.request import urlopen
             urlopen(constants.RESULT_SERVER,
                     timeout=constants.RESULT_SERVER_TIMEOUT).close()
             return True
diff --git a/atest/cli_translator.py b/atest/cli_translator.py
index abd1f85..f11b34b 100644
--- a/atest/cli_translator.py
+++ b/atest/cli_translator.py
@@ -61,14 +61,21 @@
         3. If test files found, generate Build Targets and the Run Command.
     """
 
-    def __init__(self, module_info=None):
+    def __init__(self, module_info=None, print_cache_msg=True):
         """CLITranslator constructor
 
         Args:
             module_info: ModuleInfo class that has cached module-info.json.
+            print_cache_msg: Boolean whether printing clear cache message or not.
+                             True will print message while False won't print.
         """
         self.mod_info = module_info
         self.enable_file_patterns = False
+        self.msg = ''
+        if print_cache_msg:
+            self.msg = ('(Test info has been cached for speeding up the next '
+                        'run, if test info need to be updated, please add -c '
+                        'to clean the old cache.)')
 
     # pylint: disable=too-many-locals
     def _find_test_infos(self, test, tm_test_detail):
@@ -136,9 +143,7 @@
         # non-test_mapping tests.
         if test_infos and not tm_test_detail:
             atest_utils.update_test_info_cache(test, test_infos)
-            print('Test info has been cached for speeding up the next run, if '
-                  'test info need to be updated, please add -c to clean the '
-                  'old cache.')
+            print(self.msg)
         return test_infos
 
     def _fuzzy_search_and_msg(self, test, find_test_err_msg):
diff --git a/atest/constants_default.py b/atest/constants_default.py
index 80581db..1df9e2d 100644
--- a/atest/constants_default.py
+++ b/atest/constants_default.py
@@ -182,14 +182,14 @@
 # TreeHugger TEST_MAPPING SUITE_PLANS
 TEST_MAPPING_SUITES = ['device-tests', 'general-tests']
 
-# VTS TF
-VTS_TF_MODULE = 'vts-tradefed'
+# VTS10 TF
+VTS_TF_MODULE = 'vts10-tradefed'
 
-# VTS-Core TF
-VTS_CORE_TF_MODULE = 'vts-core-tradefed'
+# VTS TF
+VTS_CORE_TF_MODULE = 'vts-tradefed'
 
 # VTS suite set
-VTS_CORE_SUITE = 'vts-core'
+VTS_CORE_SUITE = 'vts'
 
 # ATest TF
 ATEST_TF_MODULE = 'atest-tradefed'
diff --git a/atest/docs/developer_workflow.md b/atest/docs/developer_workflow.md
index 578ec1b..d3c2a32 100644
--- a/atest/docs/developer_workflow.md
+++ b/atest/docs/developer_workflow.md
@@ -6,7 +6,7 @@
 1. [Identify the code you should work on](#identify-the-code-you-should-work-on)
 2. [Working on the Python Code](#working-on-the-python-code)
 3. [Working on the TradeFed Code](#working-on-the-tradefed-code)
-4. [Working on the VTS-TradeFed Code](#working-on-the-vts-tradefed-code)
+4. [Working on the VTS10-TradeFed Code](#working-on-the-vts10-tradefed-code)
 5. [Working on the Robolectric Code](#working-on-the-robolectric-code)
 
 
@@ -44,7 +44,7 @@
 Each test runner will have a different workflow. Atest currently
 supports the following test runners:
 - TradeFed
-- VTS-TradeFed
+- VTS10-TradeFed
 - Robolectric
 
 
@@ -121,22 +121,22 @@
 
 Before submitting code you should run all the TradeFed tests.
 
-## <a name="working-on-the-vts-tradefed-code">Working on the VTS-TradeFed Code</a>
+## <a name="working-on-the-vts10-tradefed-code">Working on the VTS10-TradeFed Code</a>
 
-##### Where does the VTS-TradeFed code live?
+##### Where does the VTS10-TradeFed code live?
 
-The VTS-Tradefed code lives here: `test/vts/tools/vts-tradefed/`
+The VTS10-Tradefed code lives here: `test/vts/tools/vts-tradefed/`
 (path relative to android repo root)
 
 ##### Writing tests
 
-You shouldn't need to edit vts-tradefed code, so there is no
-need to write vts-tradefed tests. Reach out to the vts team
+You shouldn't need to edit vts10-tradefed code, so there is no
+need to write vts10 tests. Reach out to the vts team
 if you need information on their unittests.
 
 ##### Running tests
 
-Again, you shouldn't need to change vts-tradefed code.
+Again, you shouldn't need to change vts10-tradefed code.
 
 ## <a name="working-on-the-robolectric-code">Working on the Robolectric Code</a>
 
diff --git a/atest/module_info_unittest.py b/atest/module_info_unittest.py
index 2570ab2..4e48977 100755
--- a/atest/module_info_unittest.py
+++ b/atest/module_info_unittest.py
@@ -160,10 +160,10 @@
         self.assertFalse(mod_info.is_suite_in_compatibility_suites("cts", info))
         info2 = {'compatibility_suites': ["cts"]}
         self.assertTrue(mod_info.is_suite_in_compatibility_suites("cts", info2))
-        self.assertFalse(mod_info.is_suite_in_compatibility_suites("vts", info2))
-        info3 = {'compatibility_suites': ["cts", "vts"]}
+        self.assertFalse(mod_info.is_suite_in_compatibility_suites("vts10", info2))
+        info3 = {'compatibility_suites': ["cts", "vts10"]}
         self.assertTrue(mod_info.is_suite_in_compatibility_suites("cts", info3))
-        self.assertTrue(mod_info.is_suite_in_compatibility_suites("vts", info3))
+        self.assertTrue(mod_info.is_suite_in_compatibility_suites("vts10", info3))
         self.assertFalse(mod_info.is_suite_in_compatibility_suites("ats", info3))
 
     @mock.patch.object(module_info.ModuleInfo, 'is_testable_module')
diff --git a/atest/test_data/test_commands.json b/atest/test_data/test_commands.json
index bdd2390..2b6c008 100644
--- a/atest/test_data/test_commands.json
+++ b/atest/test_data/test_commands.json
@@ -36,7 +36,7 @@
 "atest_tradefed.sh template/atest_local_min --template:map test=atest --include-filter hello_world_test --log-level WARN --skip-loading-config-jar --logcat-on-failure --no-enable-granular-attempts"
 ], 
 "VtsCodelabHelloWorldTest": [
-"vts-tradefed run commandAndExit vts-staging-default -m VtsCodelabHelloWorldTest --skip-all-system-status-check --skip-preconditions --primary-abi-only"
+"vts10-tradefed run commandAndExit vts-staging-default -m VtsCodelabHelloWorldTest --skip-all-system-status-check --skip-preconditions --primary-abi-only"
 ], 
 "aidegen_unittests": [
 "atest_tradefed.sh template/atest_local_min --template:map test=atest --atest-log-file-path=/tmp/atest_run_1568627341_v33kdA/log --include-filter aidegen_unittests --log-level WARN"
@@ -56,4 +56,4 @@
 "CarMessengerRoboTests": [
 "./build/soong/soong_ui.bash --make-mode RunCarMessengerRoboTests"
 ]
-}
\ No newline at end of file
+}
diff --git a/atest/test_finders/module_finder.py b/atest/test_finders/module_finder.py
index f24e2ee..965688b 100644
--- a/atest/test_finders/module_finder.py
+++ b/atest/test_finders/module_finder.py
@@ -75,26 +75,26 @@
         return test_finder_utils.extract_test_from_tests(testable_modules)
 
     def _is_vts_module(self, module_name):
-        """Returns True if the module is a vts module, else False."""
+        """Returns True if the module is a vts10 module, else False."""
         mod_info = self.module_info.get_module_info(module_name)
         suites = []
         if mod_info:
             suites = mod_info.get('compatibility_suites', [])
         # Pull out all *ts (cts, tvts, etc) suites.
         suites = [suite for suite in suites if suite not in _SUITES_TO_IGNORE]
-        return len(suites) == 1 and 'vts' in suites
+        return len(suites) == 1 and 'vts10' in suites
 
     def _update_to_vts_test_info(self, test):
-        """Fill in the fields with vts specific info.
+        """Fill in the fields with vts10 specific info.
 
-        We need to update the runner to use the vts runner and also find the
+        We need to update the runner to use the vts10 runner and also find the
         test specific dependencies.
 
         Args:
-            test: TestInfo to update with vts specific details.
+            test: TestInfo to update with vts10 specific details.
 
         Return:
-            TestInfo that is ready for the vts test runner.
+            TestInfo that is ready for the vts10 test runner.
         """
         test.test_runner = self._VTS_TEST_RUNNER
         config_file = os.path.join(self.root_dir,
@@ -106,7 +106,7 @@
         # If we're not an absolute custom out dir, get relative out dir path.
         if custom_out_dir is None or not os.path.isabs(custom_out_dir):
             out_dir = os.path.relpath(out_dir, self.root_dir)
-        vts_out_dir = os.path.join(out_dir, 'vts', 'android-vts', 'testcases')
+        vts_out_dir = os.path.join(out_dir, 'vts10', 'android-vts10', 'testcases')
         # Parse dependency of default staging plans.
         xml_paths = test_finder_utils.search_integration_dirs(
             constants.VTS_STAGING_PLAN,
@@ -116,7 +116,7 @@
         for xml_path in xml_paths:
             vts_xmls |= test_finder_utils.get_plans_from_vts_xml(xml_path)
         for config_file in vts_xmls:
-            # Add in vts test build targets.
+            # Add in vts10 test build targets.
             test.build_targets |= test_finder_utils.get_targets_from_vts_xml(
                 config_file, vts_out_dir, self.module_info)
         test.build_targets.add('vts-test-core')
@@ -139,7 +139,7 @@
     def _process_test_info(self, test):
         """Process the test info and return some fields updated/changed.
 
-        We need to check if the test found is a special module (like vts) and
+        We need to check if the test found is a special module (like vts10) and
         update the test_info fields (like test_runner) appropriately.
 
         Args:
@@ -156,7 +156,7 @@
         test.module_class = mod_info['class']
         test.install_locations = test_finder_utils.get_install_locations(
             mod_info['installed'])
-        # Check if this is only a vts module.
+        # Check if this is only a vts10 module.
         if self._is_vts_module(test.test_name):
             return self._update_to_vts_test_info(test)
         elif self.module_info.is_robolectric_test(test.test_name):
diff --git a/atest/test_finders/module_finder_unittest.py b/atest/test_finders/module_finder_unittest.py
index e7cbf00..6d11bec 100755
--- a/atest/test_finders/module_finder_unittest.py
+++ b/atest/test_finders/module_finder_unittest.py
@@ -91,11 +91,11 @@
     def test_is_vts_module(self):
         """Test _load_module_info_file regular operation."""
         mod_name = 'mod'
-        is_vts_module_info = {'compatibility_suites': ['vts', 'tests']}
+        is_vts_module_info = {'compatibility_suites': ['vts10', 'tests']}
         self.mod_finder.module_info.get_module_info.return_value = is_vts_module_info
         self.assertTrue(self.mod_finder._is_vts_module(mod_name))
 
-        is_not_vts_module = {'compatibility_suites': ['vts', 'cts']}
+        is_not_vts_module = {'compatibility_suites': ['vts10', 'cts']}
         self.mod_finder.module_info.get_module_info.return_value = is_not_vts_module
         self.assertFalse(self.mod_finder._is_vts_module(mod_name))
 
diff --git a/atest/test_finders/test_finder_utils.py b/atest/test_finders/test_finder_utils.py
index 2e8ec64..a6a2475 100644
--- a/atest/test_finders/test_finder_utils.py
+++ b/atest/test_finders/test_finder_utils.py
@@ -586,7 +586,7 @@
 
     We're going to pull the following bits of info:
       - Parse any .apk files listed in the config file.
-      - Parse option value for "test-module-name" (for vts tests).
+      - Parse option value for "test-module-name" (for vts10 tests).
       - Look for the perf script.
 
     Args:
@@ -627,14 +627,14 @@
 
 
 def _get_vts_push_group_targets(push_file, rel_out_dir):
-    """Retrieve vts push group build targets.
+    """Retrieve vts10 push group build targets.
 
     A push group file is a file that list out test dependencies and other push
     group files. Go through the push file and gather all the test deps we need.
 
     Args:
         push_file: Name of the push file in the VTS
-        rel_out_dir: Abs path to the out dir to help create vts build targets.
+        rel_out_dir: Abs path to the out dir to help create vts10 build targets.
 
     Returns:
         Set of string which represent build targets.
@@ -678,7 +678,7 @@
 
 
 def _get_vts_binary_src_target(value, rel_out_dir):
-    """Parse out the vts binary src target.
+    """Parse out the vts10 binary src target.
 
     The value can be in the following pattern:
       - {_32bit,_64bit,_IPC32_32bit}::DATA/target (DATA/target)
@@ -727,9 +727,9 @@
     option_tags = xml_root.findall('.//include')
     if not option_tags:
         return plans
-    # Currently, all vts xmls live in the same dir :
+    # Currently, all vts10 xmls live in the same dir :
     # https://android.googlesource.com/platform/test/vts/+/master/tools/vts-tradefed/res/config/
-    # If the vts plans start using folders to organize the plans, the logic here
+    # If the vts10 plans start using folders to organize the plans, the logic here
     # should be changed.
     xml_dir = os.path.dirname(xml_file)
     for tag in option_tags:
@@ -739,9 +739,9 @@
 
 
 def get_targets_from_vts_xml(xml_file, rel_out_dir, module_info):
-    """Parse a vts xml for test dependencies we need to build.
+    """Parse a vts10 xml for test dependencies we need to build.
 
-    We have a separate vts parsing function because we make a big assumption
+    We have a separate vts10 parsing function because we make a big assumption
     on the targets (the way they're formatted and what they represent) and we
     also create these build targets in a very special manner as well.
     The 6 options we're looking for are:
@@ -754,7 +754,7 @@
 
     Args:
         module_info: ModuleInfo class used to verify targets are valid modules.
-        rel_out_dir: Abs path to the out dir to help create vts build targets.
+        rel_out_dir: Abs path to the out dir to help create vts10 build targets.
         xml_file: abs path to xml file.
 
     Returns:
@@ -770,7 +770,7 @@
             if module_info.is_module(value):
                 targets.add(value)
             else:
-                logging.warning('vts test module (%s) not present in module '
+                logging.warning('vts10 test module (%s) not present in module '
                                 'info, skipping build', value)
         elif name == _VTS_BINARY_SRC:
             targets.add(_get_vts_binary_src_target(value, rel_out_dir))
@@ -797,7 +797,7 @@
             # and then append the _VTS_TEST_FILE value to targets to build.
             target = os.path.join(rel_out_dir, value)
             # If value is just an APK, specify the path that we expect it to be in
-            # e.g. out/host/linux-x86/vts/android-vts/testcases/DATA/app/test_file/test_file.apk
+            # e.g. out/host/linux-x86/vts10/android-vts10/testcases/DATA/app/test_file/test_file.apk
             head, _ = os.path.split(value)
             if not head:
                 target = os.path.join(rel_out_dir, _VTS_OUT_DATA_APP_PATH,
diff --git a/atest/test_finders/test_info.py b/atest/test_finders/test_info.py
index d872576..707f49a 100644
--- a/atest/test_finders/test_info.py
+++ b/atest/test_finders/test_info.py
@@ -47,7 +47,7 @@
             test_finder: String of test finder.
             compatibility_suites: A list of compatibility_suites. It's a
                         snippet of compatibility_suites in module_info. e.g.
-                        ["device-tests",  "vts-core"]
+                        ["device-tests",  "vts10"]
         """
         self.test_name = test_name
         self.test_runner = test_runner
diff --git a/atest/test_runners/atest_tf_test_runner.py b/atest/test_runners/atest_tf_test_runner.py
index 5380423..6f26c6d 100644
--- a/atest/test_runners/atest_tf_test_runner.py
+++ b/atest/test_runners/atest_tf_test_runner.py
@@ -107,7 +107,7 @@
         key_path = os.path.join(self.root_dir, ape_api_key)
         if ape_api_key and os.path.exists(key_path):
             logging.debug('Set APE_API_KEY: %s', ape_api_key)
-            os.environ['APE_API_KEY'] = ape_api_key
+            os.environ['APE_API_KEY'] = key_path
         else:
             logging.debug('APE_API_KEY not set, some GTS tests may fail'
                           ' without authentication.')
diff --git a/atest/test_runners/atest_tf_test_runner_unittest.py b/atest/test_runners/atest_tf_test_runner_unittest.py
index d1968e8..5344ba0 100755
--- a/atest/test_runners/atest_tf_test_runner_unittest.py
+++ b/atest/test_runners/atest_tf_test_runner_unittest.py
@@ -306,13 +306,13 @@
         self.tr._try_set_gts_authentication_key()
         mock_exist.assert_not_called()
 
-    @mock.patch('constants.GTS_GOOGLE_SERVICE_ACCOUNT')
-    @mock.patch('os.path.exists')
-    def test_try_set_gts_authentication_key_not_set(self, mock_exist, mock_key):
+    @mock.patch('os.path.join', return_value='/tmp/file_not_exist.json')
+    def test_try_set_gts_authentication_key_not_set(self, _):
         """Test try_set_authentication_key_not_set method."""
-        # Test key neither exists nor set by user.
-        mock_exist.return_value = False
-        mock_key.return_value = ''
+        # Delete the environment variable if it's set. This is fine for this
+        # method because it's for validating the APE_API_KEY isn't set.
+        if os.environ.get('APE_API_KEY'):
+            del os.environ['APE_API_KEY']
         self.tr._try_set_gts_authentication_key()
         self.assertEqual(os.environ.get('APE_API_KEY'), None)
 
diff --git a/atest/test_runners/vts_tf_test_runner.py b/atest/test_runners/vts_tf_test_runner.py
index 9e6801b..c1f53e0 100644
--- a/atest/test_runners/vts_tf_test_runner.py
+++ b/atest/test_runners/vts_tf_test_runner.py
@@ -28,15 +28,15 @@
 class VtsTradefedTestRunner(atest_tf_test_runner.AtestTradefedTestRunner):
     """TradeFed Test Runner class."""
     NAME = 'VtsTradefedTestRunner'
-    EXECUTABLE = 'vts-tradefed'
+    EXECUTABLE = 'vts10-tradefed'
     _RUN_CMD = ('{exe} run commandAndExit {plan} -m {test} {args}')
-    _BUILD_REQ = {'vts-tradefed-standalone'}
+    _BUILD_REQ = {'vts10-tradefed-standalone'}
     _DEFAULT_ARGS = ['--skip-all-system-status-check',
                      '--skip-preconditions',
                      '--primary-abi-only']
 
     def __init__(self, results_dir, **kwargs):
-        """Init stuff for vts tradefed runner class."""
+        """Init stuff for vts10 tradefed runner class."""
         super(VtsTradefedTestRunner, self).__init__(results_dir, **kwargs)
         self.run_cmd_dict = {'exe': self.EXECUTABLE,
                              'test': '',
@@ -74,10 +74,10 @@
         return ret_code
 
     def _parse_extra_args(self, extra_args):
-        """Convert the extra args into something vts-tf can understand.
+        """Convert the extra args into something vts10-tf can understand.
 
         We want to transform the top-level args from atest into specific args
-        that vts-tradefed supports. The only arg we take as is is EXTRA_ARG
+        that vts10-tradefed supports. The only arg we take as is is EXTRA_ARG
         since that is what the user intentionally wants to pass to the test
         runner.
 
@@ -114,7 +114,7 @@
             extra_args: Dict of extra args to add to test run.
 
         Returns:
-            A List of strings that contains the vts-tradefed run command.
+            A List of strings that contains the vts10-tradefed run command.
         """
         cmds = []
         args = self._DEFAULT_ARGS
diff --git a/atest_tradefed.sh b/atest_tradefed.sh
index 4b66fdb..eb6cf53 100755
--- a/atest_tradefed.sh
+++ b/atest_tradefed.sh
@@ -34,7 +34,7 @@
           hosttestlib.jar
           cts-tradefed.jar
           sts-tradefed.jar
-          vts-core-tradefed.jar
+          vts-tradefed.jar
           vts10-tradefed.jar
           csuite-harness.jar
           tradefed-isolation.jar
diff --git a/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java b/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
index 329de8e..0d10607 100644
--- a/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
+++ b/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
@@ -593,6 +593,11 @@
         return mGceDriverParams;
     }
 
+    /** Add a param to the gce driver params. */
+    public void addGceDriverParams(String param) {
+        mGceDriverParams.add(param);
+    }
+
     /** Set the GCE driver parameter that should be paired with the build id from build info */
     public void setGceDriverBuildIdParam(String gceDriverBuildIdParam) {
         mGceDriverBuildIdParam = gceDriverBuildIdParam;
diff --git a/invocation_interfaces/com/android/tradefed/config/ConfigurationDescriptor.java b/invocation_interfaces/com/android/tradefed/config/ConfigurationDescriptor.java
index bc6d30c..e97233f 100644
--- a/invocation_interfaces/com/android/tradefed/config/ConfigurationDescriptor.java
+++ b/invocation_interfaces/com/android/tradefed/config/ConfigurationDescriptor.java
@@ -48,7 +48,7 @@
     /** Metadata key for a config to specify that it was sharded. */
     public static final String LOCAL_SHARDED_KEY = "sharded";
     /** Metadata key for a config parameterization, optional. */
-    public static final String PARAMETER_KEY = "parameter";
+    public static final String ACTIVE_PARAMETER_KEY = "active-parameter";
 
     @Option(name = "test-suite-tag", description = "A membership tag to suite. Can be repeated.")
     private List<String> mSuiteTags = new ArrayList<>();
@@ -136,7 +136,7 @@
      * @param key {@link String} of the key to add values to.
      * @param values a list of {@link String} of the additional values.
      */
-    public void addMetaData(String key, List<String> values) {
+    public void addMetadata(String key, List<String> values) {
         for (String source : values) {
             mMetaData.put(key, source);
         }
diff --git a/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java b/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
index a083a58..ddad834 100644
--- a/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
+++ b/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
@@ -37,7 +37,13 @@
         STAGE_TESTS_INDIVIDUAL_DOWNLOADS("stage_tests_individual_downloads", true),
         SHUTDOWN_HARD_LATENCY("shutdown_hard_latency_ms", false),
         DEVICE_DONE_TIMESTAMP("device_done_timestamp", false),
-        DEVICE_RELEASE_STATE("device_release_state", false);
+        DEVICE_RELEASE_STATE("device_release_state", false),
+        DEVICE_LOST_DETECTED("device_lost_detected", false),
+        SANDBOX_EXIT_CODE("sandbox_exit_code", false),
+        CF_FETCH_ARTIFACT_TIME("cf_fetch_artifact_time_ms", false),
+        CF_GCE_CREATE_TIME("cf_gce_create_time_ms", false),
+        CF_LAUNCH_CVD_TIME("cf_launch_cvd_time_ms", false),
+        CF_INSTANCE_COUNT("cf_instance_count", false);
 
         private final String mKeyName;
         // Whether or not to add the value when the key is added again.
diff --git a/invocation_interfaces/com/android/tradefed/result/TestRunResult.java b/invocation_interfaces/com/android/tradefed/result/TestRunResult.java
index 91e934e..234ef2b 100644
--- a/invocation_interfaces/com/android/tradefed/result/TestRunResult.java
+++ b/invocation_interfaces/com/android/tradefed/result/TestRunResult.java
@@ -298,6 +298,10 @@
         updateTestResult(test, TestStatus.FAILURE, FailureDescription.create(trace));
     }
 
+    public void testFailed(TestDescription test, FailureDescription failure) {
+        updateTestResult(test, TestStatus.FAILURE, failure);
+    }
+
     public void testAssumptionFailure(TestDescription test, String trace) {
         updateTestResult(test, TestStatus.ASSUMPTION_FAILURE, FailureDescription.create(trace));
     }
diff --git a/proto/test_record.proto b/proto/test_record.proto
index 96553c7..6f5a6ae 100644
--- a/proto/test_record.proto
+++ b/proto/test_record.proto
@@ -127,4 +127,7 @@
 
   // A free-formed text that can help debugging the issue at hand.
   string debug_help_message = 10;
-}
\ No newline at end of file
+
+  // The fully-qualified name of the exception class associated with the error.
+  string error_type = 20;
+}
diff --git a/src/com/android/tradefed/build/FileDownloadCache.java b/src/com/android/tradefed/build/FileDownloadCache.java
index 1b8d68f..d79054e 100644
--- a/src/com/android/tradefed/build/FileDownloadCache.java
+++ b/src/com/android/tradefed/build/FileDownloadCache.java
@@ -328,6 +328,9 @@
             throws BuildRetrievalError {
         boolean download = false;
         File cachedFile, copyFile;
+        if (remotePath == null) {
+            throw new BuildRetrievalError("remote path was null.");
+        }
 
         lockFile(remotePath);
         try {
diff --git a/src/com/android/tradefed/device/DeviceManager.java b/src/com/android/tradefed/device/DeviceManager.java
index 390e636..7e08c4f 100644
--- a/src/com/android/tradefed/device/DeviceManager.java
+++ b/src/com/android/tradefed/device/DeviceManager.java
@@ -108,7 +108,7 @@
      *
      * <p>serial2 offline
      */
-    private static final String DEVICE_LIST_PATTERN = ".*\n(%s)\\s+(device|offline).*";
+    private static final String DEVICE_LIST_PATTERN = ".*\n(%s)\\s+(device|offline|recovery).*";
 
     private Semaphore mConcurrentFlashLock = null;
 
@@ -619,6 +619,8 @@
         if (d != null) {
             DeviceEventResponse r = d.handleAllocationEvent(DeviceEvent.FORCE_ALLOCATE_REQUEST);
             if (r.stateChanged && r.allocationState == DeviceAllocationState.Allocated) {
+                // Wait for the fastboot state to be updated once to update the IDevice.
+                d.getMonitor().waitForDeviceBootloaderStateUpdate();
                 return d;
             }
         }
diff --git a/src/com/android/tradefed/device/cloud/GceAvdInfo.java b/src/com/android/tradefed/device/cloud/GceAvdInfo.java
index 5e9e5c4..4d627f2 100644
--- a/src/com/android/tradefed/device/cloud/GceAvdInfo.java
+++ b/src/com/android/tradefed/device/cloud/GceAvdInfo.java
@@ -16,10 +16,13 @@
 package com.android.tradefed.device.cloud;
 
 import com.android.tradefed.command.remote.DeviceDescriptor;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.targetprep.TargetSetupError;
 import com.android.tradefed.util.FileUtil;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.common.net.HostAndPort;
 
@@ -184,6 +187,7 @@
             if (devices != null) {
                 if (devices.length() == 1) {
                     JSONObject d = (JSONObject) devices.get(0);
+                    addCfStartTimeMetrics(d);
                     String ip = d.getString("ip");
                     String instanceName = d.getString("instance_name");
                     GceAvdInfo avdInfo =
@@ -225,4 +229,31 @@
         }
         return res;
     }
+
+    @VisibleForTesting
+    static void addCfStartTimeMetrics(JSONObject json) {
+        // These metrics may not be available for all GCE.
+        String fetch_artifact_time = json.optString("fetch_artifact_time");
+        if (!fetch_artifact_time.isEmpty()) {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.CF_FETCH_ARTIFACT_TIME,
+                    Double.valueOf(Double.parseDouble(fetch_artifact_time) * 1000).longValue());
+        }
+        String gce_create_time = json.optString("gce_create_time");
+        if (!gce_create_time.isEmpty()) {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.CF_GCE_CREATE_TIME,
+                    Double.valueOf(Double.parseDouble(gce_create_time) * 1000).longValue());
+        }
+        String launch_cvd_time = json.optString("launch_cvd_time");
+        if (!launch_cvd_time.isEmpty()) {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.CF_LAUNCH_CVD_TIME,
+                    Double.valueOf(Double.parseDouble(launch_cvd_time) * 1000).longValue());
+        }
+        if (!InvocationMetricLogger.getInvocationMetrics()
+                .containsKey(InvocationMetricKey.CF_INSTANCE_COUNT.toString())) {
+            InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.CF_INSTANCE_COUNT, 1);
+        }
+    }
 }
diff --git a/src/com/android/tradefed/invoker/InvocationExecution.java b/src/com/android/tradefed/invoker/InvocationExecution.java
index fedee05..a1dcecf 100644
--- a/src/com/android/tradefed/invoker/InvocationExecution.java
+++ b/src/com/android/tradefed/invoker/InvocationExecution.java
@@ -231,6 +231,7 @@
                         new ParallelDeviceExecutor<>(testInfo.getContext().getDevices());
                 List<Callable<Boolean>> callableTasks = new ArrayList<>();
                 for (String deviceName : testInfo.getContext().getDeviceConfigNames()) {
+                    mTrackTargetPreparers.put(deviceName, new HashSet<>());
                     final int deviceIndex = index;
                     // Replicate TestInfo
                     TestInformation replicated =
@@ -330,6 +331,7 @@
     public final void runDevicePreInvocationSetup(
             IInvocationContext context, IConfiguration config, ITestLogger logger)
             throws DeviceNotAvailableException, TargetSetupError {
+        customizeDevicePreInvocation(config, context);
         for (String deviceName : context.getDeviceConfigNames()) {
             ITestDevice device = context.getDevice(deviceName);
 
@@ -346,6 +348,16 @@
         }
     }
 
+    /**
+     * Give a chance to customize some of the device before preInvocationSetup.
+     *
+     * @param config The config of the invocation.
+     * @param context The current invocation context.
+     */
+    protected void customizeDevicePreInvocation(IConfiguration config, IInvocationContext context) {
+        // Empty by default
+    }
+
     /** {@inheritDoc} */
     @Override
     public final void runDevicePostInvocationTearDown(
diff --git a/src/com/android/tradefed/invoker/RemoteInvocationExecution.java b/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
index 99d9ab2..d7843d9 100644
--- a/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
+++ b/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
@@ -30,11 +30,14 @@
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.DeviceSelectionOptions;
+import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.TestDeviceOptions;
 import com.android.tradefed.device.cloud.GceAvdInfo;
 import com.android.tradefed.device.cloud.GceManager;
 import com.android.tradefed.device.cloud.ManagedRemoteDevice;
 import com.android.tradefed.device.cloud.RemoteFileUtil;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.FileInputStreamSource;
@@ -117,6 +120,23 @@
     }
 
     @Override
+    protected void customizeDevicePreInvocation(IConfiguration config, IInvocationContext context) {
+        super.customizeDevicePreInvocation(config, context);
+
+        if (config.getCommandOptions().getShardCount() != null
+                && config.getCommandOptions().getShardIndex() == null) {
+            ITestDevice device = context.getDevices().get(0);
+            TestDeviceOptions options = device.getOptions();
+            // Trigger the multi-tenant start in the VM
+            options.addGceDriverParams("--num-avds-per-instance");
+            String count = config.getCommandOptions().getShardCount().toString();
+            options.addGceDriverParams(count);
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.CF_INSTANCE_COUNT, count);
+        }
+    }
+
+    @Override
     public void runTests(
             TestInformation info, IConfiguration config, ITestInvocationListener listener)
             throws Throwable {
diff --git a/src/com/android/tradefed/invoker/TestInvocation.java b/src/com/android/tradefed/invoker/TestInvocation.java
index 46edad7..2bf69ee 100644
--- a/src/com/android/tradefed/invoker/TestInvocation.java
+++ b/src/com/android/tradefed/invoker/TestInvocation.java
@@ -341,17 +341,26 @@
             }
             mStatus = "done running tests";
             // Track the timestamp when we are done with devices
-            InvocationMetricLogger.addInvocationMetrics(
+            addInvocationMetric(
                     InvocationMetricKey.DEVICE_DONE_TIMESTAMP, System.currentTimeMillis());
-            if (config.getCommandOptions().earlyDeviceRelease()) {
-                // Capture the FreeDeviceState of the primary device
-                Map<ITestDevice, FreeDeviceState> devicesStates =
-                        CommandScheduler.createReleaseMap(context, exception);
-                if (devicesStates.size() >= 1) {
-                    InvocationMetricLogger.addInvocationMetrics(
-                            InvocationMetricKey.DEVICE_RELEASE_STATE,
-                            devicesStates.values().iterator().next().toString());
+            // Capture the FreeDeviceState of the primary device
+            Map<ITestDevice, FreeDeviceState> devicesStates =
+                    CommandScheduler.createReleaseMap(context, exception);
+            if (devicesStates.size() >= 1) {
+                addInvocationMetric(
+                        InvocationMetricKey.DEVICE_RELEASE_STATE,
+                        devicesStates.values().iterator().next().toString());
+            }
+            int countLost = 0;
+            for (FreeDeviceState fds : devicesStates.values()) {
+                if (FreeDeviceState.UNAVAILABLE.equals(fds)) {
+                    countLost++;
                 }
+            }
+            if (countLost > 0) {
+                addInvocationMetric(InvocationMetricKey.DEVICE_LOST_DETECTED, countLost);
+            }
+            if (config.getCommandOptions().earlyDeviceRelease()) {
                 for (IScheduledInvocationListener scheduleListener : mSchedulerListeners) {
                     scheduleListener.releaseDevices(context, devicesStates);
                 }
@@ -921,6 +930,14 @@
                 .setLastInvocationExitCode(code, stack);
     }
 
+    protected void addInvocationMetric(InvocationMetricKey key, long value) {
+        InvocationMetricLogger.addInvocationMetrics(key, value);
+    }
+
+    protected void addInvocationMetric(InvocationMetricKey key, String value) {
+        InvocationMetricLogger.addInvocationMetrics(key, value);
+    }
+
     public static String getDeviceLogName(Stage stage) {
         return DEVICE_LOG_NAME_PREFIX + stage.getName();
     }
diff --git a/src/com/android/tradefed/log/BaseLeveledLogOutput.java b/src/com/android/tradefed/log/BaseLeveledLogOutput.java
index c22468d..91219cb 100644
--- a/src/com/android/tradefed/log/BaseLeveledLogOutput.java
+++ b/src/com/android/tradefed/log/BaseLeveledLogOutput.java
@@ -20,6 +20,8 @@
 import com.android.tradefed.config.Option;
 import com.android.tradefed.log.LogUtil.CLog;
 
+import com.google.common.collect.ImmutableMap;
+
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -52,6 +54,15 @@
     )
     private Map<String, LogLevel> mComponentVerbosity = new HashMap<>();
 
+    @Option(
+            name = "force-verbosity-map",
+            description = "Enforce a pre-set verbosity of some component to avoid extreme logging.")
+    private boolean mEnableForcedVerbosity = true;
+
+    // Add components we have less control over (ddmlib for example) to ensure they don't flood
+    // us. This will still write to the log.
+    private Map<String, LogLevel> mForcedVerbosity = ImmutableMap.of("ddms", LogLevel.WARN);
+
     private Map<String, LogLevel> mVerbosityMap = new HashMap<>();
 
     /** Initialize the components filters based on the invocation {@link IConfiguration}. */
@@ -102,12 +113,22 @@
                 receiver.put(objTag, components.get(component));
             }
         }
-        // Add components we have less control over (ddmlib for example) to ensure they don't flood
-        // us. This will still write to the log.
-        mVerbosityMap.put("ddms", LogLevel.WARN);
+        if (shouldForceVerbosity()) {
+            mVerbosityMap.putAll(mForcedVerbosity);
+        }
     }
 
     /** {@inheritDoc} */
     @Override
     public abstract ILeveledLogOutput clone();
+
+    /** Whether or not to enforce the verbosity map. */
+    public boolean shouldForceVerbosity() {
+        return mEnableForcedVerbosity;
+    }
+
+    /** Returns the map of the forced verbosity. */
+    public Map<String, LogLevel> getForcedVerbosityMap() {
+        return mForcedVerbosity;
+    }
 }
diff --git a/src/com/android/tradefed/log/BaseStreamLogger.java b/src/com/android/tradefed/log/BaseStreamLogger.java
index 5027386..ec5adb6 100644
--- a/src/com/android/tradefed/log/BaseStreamLogger.java
+++ b/src/com/android/tradefed/log/BaseStreamLogger.java
@@ -22,22 +22,10 @@
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.util.HashMap;
-import java.util.Map;
 
 /** A {@link ILeveledLogOutput} that directs log messages to an output stream and to stdout. */
 public abstract class BaseStreamLogger<OS extends OutputStream> extends BaseLeveledLogOutput {
 
-    /**
-     * Map of log tag to a level they are forced at for writing to log file purpose. This ensure
-     * that some logs we have less control over can still be regulated.
-     */
-    private static final Map<String, LogLevel> FORCED_LOG_LEVEL = new HashMap<>();
-
-    static {
-        FORCED_LOG_LEVEL.put("ddms", LogLevel.WARN);
-    }
-
     @Option(name = "log-level", description = "the minimum log level to log.")
     private LogLevel mLogLevel = LogLevel.DEBUG;
 
@@ -109,8 +97,8 @@
 
     // Determines whether a message should be written to the output stream.
     private boolean shouldWrite(String tag, LogLevel messageLogLevel, LogLevel invocationLogLevel) {
-        LogLevel forcedLevel = FORCED_LOG_LEVEL.get(tag);
-        if (forcedLevel == null) {
+        LogLevel forcedLevel = getForcedVerbosityMap().get(tag);
+        if (forcedLevel == null || !shouldForceVerbosity()) {
             return true;
         }
         // Use the highest level of our forced and invocation to decide if we should log the
diff --git a/src/com/android/tradefed/result/CollectingTestListener.java b/src/com/android/tradefed/result/CollectingTestListener.java
index 7f46ee1..e12902a 100644
--- a/src/com/android/tradefed/result/CollectingTestListener.java
+++ b/src/com/android/tradefed/result/CollectingTestListener.java
@@ -298,6 +298,12 @@
     }
 
     @Override
+    public void testFailed(TestDescription test, FailureDescription failure) {
+        setCountDirty();
+        mCurrentTestRunResult.testFailed(test, failure);
+    }
+
+    @Override
     public void testAssumptionFailure(TestDescription test, String trace) {
         setCountDirty();
         mCurrentTestRunResult.testAssumptionFailure(test, trace);
diff --git a/src/com/android/tradefed/result/ConsoleResultReporter.java b/src/com/android/tradefed/result/ConsoleResultReporter.java
index 6cedd57..1955b7d 100644
--- a/src/com/android/tradefed/result/ConsoleResultReporter.java
+++ b/src/com/android/tradefed/result/ConsoleResultReporter.java
@@ -125,28 +125,33 @@
     public void invocationEnded(long elapsedTime) {
         int[] results = mResultCountListener.getResultCounts();
         StringBuilder sb = new StringBuilder();
+        sb.append("========== Result Summary ==========");
         sb.append(String.format("\nResults summary for test-tag '%s': ", mTestTag));
         sb.append(mResultCountListener.getTotalTests());
         sb.append(" Tests [");
         sb.append(results[TestStatus.PASSED.ordinal()]);
-        sb.append(" Passed ");
+        sb.append(" Passed");
         if (results[TestStatus.FAILURE.ordinal()] > 0) {
+            sb.append(" ");
             sb.append(results[TestStatus.FAILURE.ordinal()]);
-            sb.append(" Failed ");
+            sb.append(" Failed");
         }
         if (results[TestStatus.IGNORED.ordinal()] > 0) {
+            sb.append(" ");
             sb.append(results[TestStatus.IGNORED.ordinal()]);
-            sb.append(" Ignored ");
+            sb.append(" Ignored");
         }
         if (results[TestStatus.ASSUMPTION_FAILURE.ordinal()] > 0) {
+            sb.append(" ");
             sb.append(results[TestStatus.ASSUMPTION_FAILURE.ordinal()]);
-            sb.append(" Assumption failures ");
+            sb.append(" Assumption failures");
         }
         if (results[TestStatus.INCOMPLETE.ordinal()] > 0) {
+            sb.append(" ");
             sb.append(results[TestStatus.INCOMPLETE.ordinal()]);
             sb.append(" Incomplete");
         }
-        sb.append("\r\n");
+        sb.append("] \r\n");
         print(sb.toString());
         if (mDisplayFailureSummary) {
             for (Entry<TestDescription, TestResult> entry : mFailures.entrySet()) {
diff --git a/src/com/android/tradefed/result/proto/ProtoResultParser.java b/src/com/android/tradefed/result/proto/ProtoResultParser.java
index bb07623..0f775f9 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultParser.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultParser.java
@@ -36,9 +36,12 @@
 import com.android.tradefed.result.proto.LogFileProto.LogFileInfo;
 import com.android.tradefed.result.proto.TestRecordProto.ChildReference;
 import com.android.tradefed.result.proto.TestRecordProto.DebugInfo;
+import com.android.tradefed.result.proto.TestRecordProto.DebugInfoContext;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.result.proto.TestRecordProto.TestRecord;
 import com.android.tradefed.testtype.suite.ModuleDefinition;
 import com.android.tradefed.util.MultiMap;
+import com.android.tradefed.util.SerializationUtil;
 import com.android.tradefed.util.proto.TestRecordProtoUtil;
 
 import com.google.common.base.Splitter;
@@ -75,6 +78,8 @@
     private boolean mInvocationStarted = false;
     private boolean mInvocationEnded = false;
     private boolean mFirstModule = true;
+    /** Track the name of the module in progress. */
+    private String mModuleInProgress = null;
 
     /** Ctor. */
     public ProtoResultParser(
@@ -187,6 +192,26 @@
         return mInvocationEnded;
     }
 
+    /** Returns the id of the module in progress. Returns null if none in progress. */
+    public String getModuleInProgress() {
+        return mModuleInProgress;
+    }
+
+    /** If needed to ensure consistent reporting, complete the events of the module. */
+    public void completeModuleEvents() {
+        if (getModuleInProgress() == null) {
+            return;
+        }
+        mListener.testRunStarted(getModuleInProgress(), 0);
+        FailureDescription failure =
+                FailureDescription.create(
+                        "Module was interrupted after starting, results are incomplete.",
+                        FailureStatus.INFRA_FAILURE);
+        mListener.testRunFailed(failure);
+        mListener.testRunEnded(0L, new HashMap<String, Metric>());
+        mListener.testModuleEnded();
+    }
+
     private void evalChildrenProto(List<ChildReference> children, boolean isInRun) {
         for (ChildReference child : children) {
             TestRecord childProto = child.getInlineTestRecord();
@@ -274,9 +299,24 @@
         }
 
         if (endInvocationProto.hasDebugInfo()) {
-            // TODO: Re-interpret the exception with proper type.
             String trace = endInvocationProto.getDebugInfo().getTrace();
-            mListener.invocationFailed(new Throwable(trace));
+            Throwable invocationError = new Throwable(trace);
+            if (endInvocationProto.getDebugInfo().hasDebugInfoContext()) {
+                DebugInfoContext failureContext =
+                        endInvocationProto.getDebugInfo().getDebugInfoContext();
+                if (!Strings.isNullOrEmpty(failureContext.getErrorType())) {
+                    try {
+                        invocationError =
+                                (Throwable)
+                                        SerializationUtil.deserialize(
+                                                failureContext.getErrorType());
+                    } catch (IOException e) {
+                        CLog.e("Failed to deserialize the invocation exception:");
+                        CLog.e(e);
+                    }
+                }
+            }
+            mListener.invocationFailed(invocationError);
         }
 
         log("Invocation ended proto");
@@ -311,12 +351,13 @@
                     InvocationContext.fromProto(anyDescription.unpack(Context.class));
             String message = "Test module started proto";
             if (moduleContext.getAttributes().containsKey(ModuleDefinition.MODULE_ID)) {
-                message +=
-                        (": "
-                                + moduleContext
-                                        .getAttributes()
-                                        .getUniqueMap()
-                                        .get(ModuleDefinition.MODULE_ID));
+                String moduleId =
+                        moduleContext
+                                .getAttributes()
+                                .getUniqueMap()
+                                .get(ModuleDefinition.MODULE_ID);
+                message += (": " + moduleId);
+                mModuleInProgress = moduleId;
             }
             log(message);
             mListener.testModuleStarted(moduleContext);
@@ -334,6 +375,7 @@
         handleLogs(moduleProto);
         log("Test module ended proto");
         mListener.testModuleEnded();
+        mModuleInProgress = null;
     }
 
     /** Handles the test run level of the invocation. */
diff --git a/src/com/android/tradefed/result/proto/ProtoResultReporter.java b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
index 6ba026f..4e77622 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
@@ -33,12 +33,14 @@
 import com.android.tradefed.result.proto.TestRecordProto.TestStatus;
 import com.android.tradefed.result.retry.ISupportGranularResults;
 import com.android.tradefed.testtype.suite.ModuleDefinition;
+import com.android.tradefed.util.SerializationUtil;
 import com.android.tradefed.util.StreamUtil;
 
 import com.google.common.base.Strings;
 import com.google.protobuf.Any;
 import com.google.protobuf.Timestamp;
 
+import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Stack;
@@ -192,6 +194,14 @@
                 debugBuilder.setErrorMessage(mInvocationFailure.getMessage());
             }
             debugBuilder.setTrace(StreamUtil.getStackTrace(mInvocationFailure));
+            DebugInfoContext.Builder debugContext = DebugInfoContext.newBuilder();
+            try {
+                debugContext.setErrorType(SerializationUtil.serializeToString(mInvocationFailure));
+            } catch (IOException e) {
+                CLog.e("Failed to serialize the invocation failure:");
+                CLog.e(e);
+            }
+            debugBuilder.setDebugInfoContext(debugContext);
             mInvocationRecordBuilder.setDebugInfo(debugBuilder);
         }
 
diff --git a/src/com/android/tradefed/result/proto/StreamProtoReceiver.java b/src/com/android/tradefed/result/proto/StreamProtoReceiver.java
index adef18b..f674906 100644
--- a/src/com/android/tradefed/result/proto/StreamProtoReceiver.java
+++ b/src/com/android/tradefed/result/proto/StreamProtoReceiver.java
@@ -49,6 +49,12 @@
     private long mExtraWaitTimeForEvents = 0L;
 
     /**
+     * Stop parsing events when this is set. This allows to avoid a thread parsing the events when
+     * we don't expect them anymore.
+     */
+    private boolean mStopParsing = false;
+
+    /**
      * Ctor.
      *
      * @param listener the {@link ITestInvocationListener} where to report the results.
@@ -184,12 +190,25 @@
             } catch (InterruptedException e) {
                 CLog.e(e);
                 throw new RuntimeException(e);
+            } finally {
+                mStopParsing = true;
             }
         }
         return true;
     }
 
+    /** If needed to ensure consistent reporting, complete the events of the module. */
+    public void completeModuleEvents() {
+        mParser.completeModuleEvents();
+    }
+
     private void parse(TestRecord receivedRecord) {
+        if (mStopParsing) {
+            CLog.i(
+                    "Skip parsing of %s. It came after joinReceiver.",
+                    receivedRecord.getTestRecordId());
+            return;
+        }
         try {
             TestLevel level = mParser.processNewProto(receivedRecord);
             if (TestLevel.MODULE.equals(level)) {
diff --git a/src/com/android/tradefed/sandbox/SandboxConfigDump.java b/src/com/android/tradefed/sandbox/SandboxConfigDump.java
index 6b6b7e8..2395bbb 100644
--- a/src/com/android/tradefed/sandbox/SandboxConfigDump.java
+++ b/src/com/android/tradefed/sandbox/SandboxConfigDump.java
@@ -106,10 +106,6 @@
                     // Ensure we get the stdout logging in FileLogger case.
                     ((FileLogger) logger).setLogLevelDisplay(LogLevel.VERBOSE);
                 }
-                // Turn off some of the invocation level options that would be duplicated in the
-                // parent.
-                config.getCommandOptions().setBugreportOnInvocationEnded(false);
-                config.getCommandOptions().setBugreportzOnInvocationEnded(false);
 
                 // Ensure in special conditions (placeholder devices) we can still allocate.
                 secureDeviceAllocation(config);
diff --git a/src/com/android/tradefed/sandbox/SandboxInvocationRunner.java b/src/com/android/tradefed/sandbox/SandboxInvocationRunner.java
index 96cb4f7..e90ff43 100644
--- a/src/com/android/tradefed/sandbox/SandboxInvocationRunner.java
+++ b/src/com/android/tradefed/sandbox/SandboxInvocationRunner.java
@@ -18,6 +18,8 @@
 import com.android.tradefed.config.Configuration;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.util.CommandResult;
@@ -60,6 +62,11 @@
         PrettyPrintDelimiter.printStageDelimiter("Done with Sandbox Environment Setup");
         try {
             CommandResult result = sandbox.run(config, listener);
+            if (result.getExitCode() != null) {
+                // Log the exit code
+                InvocationMetricLogger.addInvocationMetrics(
+                        InvocationMetricKey.SANDBOX_EXIT_CODE, result.getExitCode());
+            }
             if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
                 CLog.e(
                         "Sandbox finished with status: %s and exit code: %s",
diff --git a/src/com/android/tradefed/sandbox/TradefedSandbox.java b/src/com/android/tradefed/sandbox/TradefedSandbox.java
index 83572ca..e45b91e 100644
--- a/src/com/android/tradefed/sandbox/TradefedSandbox.java
+++ b/src/com/android/tradefed/sandbox/TradefedSandbox.java
@@ -120,7 +120,8 @@
         }
 
         long timeout = config.getCommandOptions().getInvocationTimeout();
-        mRunUtil.allowInterrupt(false);
+        // Allow interruption, subprocess should handle signals itself
+        mRunUtil.allowInterrupt(true);
         CommandResult result =
                 mRunUtil.runTimedCmd(timeout, mStdout, mStderr, mCmdArgs.toArray(new String[0]));
         // Log stdout and stderr
@@ -169,6 +170,9 @@
             result.setStderr(
                     String.format("Event receiver thread did not complete.:\n%s", stderrText));
         }
+        if (mProtoReceiver != null) {
+            mProtoReceiver.completeModuleEvents();
+        }
         PrettyPrintDelimiter.printStageDelimiter(
                 String.format(
                         "Execution of the tests occurred in the sandbox, you can find its logs "
@@ -378,6 +382,10 @@
                 }
                 throw e;
             }
+            // Turn off some of the invocation level options that would be duplicated in the
+            // child sandbox subprocess.
+            config.getCommandOptions().setBugreportOnInvocationEnded(false);
+            config.getCommandOptions().setBugreportzOnInvocationEnded(false);
         } catch (IOException | ConfigurationException e) {
             StreamUtil.close(mEventParser);
             StreamUtil.close(mProtoReceiver);
diff --git a/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java b/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java
index f097786..14f8e78 100644
--- a/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java
+++ b/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java
@@ -92,6 +92,11 @@
         mTestDevice.executeShellCommand("stop");
     }
 
+    private void startDeviceRuntime() throws DeviceNotAvailableException {
+        mTestDevice.executeShellCommand("start");
+        mTestDevice.waitForDeviceAvailable();
+    }
+
     /** {@inheritDoc} */
     @Override
     public void run(TestInformation testInfo, ITestInvocationListener listener)
@@ -171,6 +176,12 @@
             }
             batteryLevel = newLevel;
         }
+
+        if (mStopRuntime) {
+            // Restart runtime if it was stopped
+            startDeviceRuntime();
+        }
+
         CLog.w("Device %s is now charged to battery level %d; releasing.",
                 mTestDevice.getSerialNumber(), batteryLevel);
     }
diff --git a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
index 0c8bc66..550ac65 100644
--- a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
@@ -180,6 +180,13 @@
     )
     private Set<ModuleParameters> mExcludedModuleParameters = new HashSet<>();
 
+    @Option(
+            name = "fail-on-everything-filtered",
+            description =
+                    "Whether or not to fail the invocation in case test filter returns"
+                            + " an empty result.")
+    private boolean mFailOnEverythingFiltered = false;
+
     private SuiteModuleLoader mModuleRepo;
     private Map<String, List<SuiteTestFilter>> mIncludeFiltersParsed = new HashMap<>();
     private Map<String, List<SuiteTestFilter>> mExcludeFiltersParsed = new HashMap<>();
@@ -285,7 +292,19 @@
             // Finally add the full test cases directory in case there is no special sub-dir.
             testsDirectories.add(testsDir);
             // Actual loading of the configurations.
-            return loadingStrategy(abis, testsDirectories, mSuitePrefix, mSuiteTag);
+            LinkedHashMap<String, IConfiguration> loadedTests =
+                    loadingStrategy(abis, testsDirectories, mSuitePrefix, mSuiteTag);
+
+            if (mFailOnEverythingFiltered
+                    && loadedTests.isEmpty()
+                    && !mIncludeFiltersParsed.isEmpty()) {
+                throw new IllegalStateException(
+                        String.format(
+                                "Include filter '%s' was specified"
+                                        + " but resulted in an empty test set.",
+                                includeFilter));
+            }
+            return loadedTests;
         } catch (DeviceNotAvailableException | FileNotFoundException e) {
             throw new RuntimeException(e);
         }
diff --git a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
index 192aff6..d869243 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
@@ -188,7 +188,7 @@
                 configDescriptor
                         .getAllMetaData()
                         .getUniqueMap()
-                        .get(ConfigurationDescriptor.PARAMETER_KEY);
+                        .get(ConfigurationDescriptor.ACTIVE_PARAMETER_KEY);
         if (parameterization != null) {
             mModuleInvocationContext.addInvocationAttribute(
                     MODULE_PARAMETERIZATION, parameterization);
diff --git a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
index df958aa..f233b8d 100644
--- a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
+++ b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
@@ -328,7 +328,7 @@
                             paramConfig
                                     .getConfigurationDescription()
                                     .addMetadata(
-                                            ConfigurationDescriptor.PARAMETER_KEY,
+                                            ConfigurationDescriptor.ACTIVE_PARAMETER_KEY,
                                             param.getParameterIdentifier());
                             param.addParameterSpecificConfig(paramConfig);
                             setUpConfig(name, baseId, fullId, paramConfig, abi);
diff --git a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
index 037ceeb..1f3514a 100644
--- a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
+++ b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
@@ -177,7 +177,7 @@
                 moduleConfig.setTests(allTests);
                 // Set test sources to ConfigurationDescriptor.
                 List<String> testSources = getTestSources(testInfos);
-                configDescriptor.addMetaData(TestMapping.TEST_SOURCES, testSources);
+                configDescriptor.addMetadata(TestMapping.TEST_SOURCES, testSources);
             }
         }
         return testConfigs;
diff --git a/src/com/android/tradefed/util/GCSFileDownloader.java b/src/com/android/tradefed/util/GCSFileDownloader.java
index 303d3b7..08c4990 100644
--- a/src/com/android/tradefed/util/GCSFileDownloader.java
+++ b/src/com/android/tradefed/util/GCSFileDownloader.java
@@ -190,11 +190,16 @@
         }
         for (String subRemoteFolder : subRemoteFolders) {
             String subFolderName = Paths.get(subRemoteFolder).getFileName().toString();
-            if (!recursiveCheckFolderFreshness(
-                    bucketName, subRemoteFolder, new File(localFolder, subFolderName))) {
+            File subFolder = new File(localFolder, subFolderName);
+            if (new File(localFolder, subFolderName).exists()
+                    && !new File(localFolder, subFolderName).isDirectory()) {
+                CLog.w("%s exists as a non-directory.", subFolder);
+                subFolder = new File(localFolder, subFolderName + "_folder");
+            }
+            if (!recursiveCheckFolderFreshness(bucketName, subRemoteFolder, subFolder)) {
                 return false;
             }
-            subFilenames.remove(subFolderName);
+            subFilenames.remove(subFolder.getName());
         }
         return subFilenames.isEmpty();
     }
@@ -310,6 +315,14 @@
         if (!localFolder.exists()) {
             FileUtil.mkdirsRWX(localFolder);
         }
+        if (!localFolder.isDirectory()) {
+            String error =
+                    String.format(
+                            "%s is not a folder. (gs://%s/%s)",
+                            localFolder, bucketName, remoteFolderName);
+            CLog.e(error);
+            throw new IOException(error);
+        }
         Set<String> subFilenames = new HashSet<>(Arrays.asList(localFolder.list()));
         List<String> subRemoteFolders = new ArrayList<>();
         List<StorageObject> subRemoteFiles = new ArrayList<>();
@@ -322,9 +335,14 @@
         }
         for (String subRemoteFolder : subRemoteFolders) {
             String subFolderName = Paths.get(subRemoteFolder).getFileName().toString();
-            recursiveDownloadFolder(
-                    bucketName, subRemoteFolder, new File(localFolder, subFolderName));
-            subFilenames.remove(subFolderName);
+            File subFolder = new File(localFolder, subFolderName);
+            if (new File(localFolder, subFolderName).exists()
+                    && !new File(localFolder, subFolderName).isDirectory()) {
+                CLog.w("%s exists as a non-directory.", subFolder);
+                subFolder = new File(localFolder, subFolderName + "_folder");
+            }
+            recursiveDownloadFolder(bucketName, subRemoteFolder, subFolder);
+            subFilenames.remove(subFolder.getName());
         }
         for (String subFilename : subFilenames) {
             FileUtil.recursiveDelete(new File(localFolder, subFilename));
diff --git a/src/com/android/tradefed/util/JUnitXmlParser.java b/src/com/android/tradefed/util/JUnitXmlParser.java
index 03253e5..ccc4560 100644
--- a/src/com/android/tradefed/util/JUnitXmlParser.java
+++ b/src/com/android/tradefed/util/JUnitXmlParser.java
@@ -106,6 +106,11 @@
             if (FAILURE_TAG.equalsIgnoreCase(name) || ERROR_TAG.equalsIgnoreCase(name)) {
                 // current testcase has a failure - will be extracted in characters() callback
                 mFailureContent = new StringBuffer();
+                String value = attributes.getValue("message");
+                if (value != null) {
+                    mFailureContent.append(value);
+                    mFailureContent.append(". ");
+                }
             }
         }
 
diff --git a/src/com/android/tradefed/util/LocalRunInstructionBuilder.java b/src/com/android/tradefed/util/LocalRunInstructionBuilder.java
index 63c72f0..3550df9 100644
--- a/src/com/android/tradefed/util/LocalRunInstructionBuilder.java
+++ b/src/com/android/tradefed/util/LocalRunInstructionBuilder.java
@@ -130,7 +130,7 @@
         }
         // Ensure repro is aligned with parameterized modules.
         List<String> paramMetadata =
-                configDescriptor.getMetaData(ConfigurationDescriptor.PARAMETER_KEY);
+                configDescriptor.getMetaData(ConfigurationDescriptor.ACTIVE_PARAMETER_KEY);
         if (paramMetadata != null
                 && paramMetadata.size() > 0
                 && "instant".equals(paramMetadata.get(0))) {
diff --git a/src/com/android/tradefed/util/SerializationUtil.java b/src/com/android/tradefed/util/SerializationUtil.java
index 0c991ff..0bd3fed 100644
--- a/src/com/android/tradefed/util/SerializationUtil.java
+++ b/src/com/android/tradefed/util/SerializationUtil.java
@@ -15,6 +15,8 @@
  */
 package com.android.tradefed.util;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -22,6 +24,7 @@
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
+import java.util.Base64;
 
 /** Utility to serialize/deserialize an object that implements {@link Serializable}. */
 public class SerializationUtil {
@@ -53,6 +56,49 @@
     }
 
     /**
+     * Serialize and object into a base64 encoded string.
+     *
+     * @param o the object to serialize.
+     * @return the {@link String} where the object was serialized.
+     * @throws IOException if serialization fails.
+     */
+    public static String serializeToString(Serializable o) throws IOException {
+        ByteArrayOutputStream byteOut = null;
+        ObjectOutputStream out = null;
+        try {
+            byteOut = new ByteArrayOutputStream();
+            out = new ObjectOutputStream(byteOut);
+            out.writeObject(o);
+            return Base64.getEncoder().encodeToString(byteOut.toByteArray());
+        } finally {
+            StreamUtil.close(out);
+            StreamUtil.close(byteOut);
+        }
+    }
+
+    /**
+     * Deserialize an object that was serialized using {@link #serializeToString(Serializable)}.
+     *
+     * @param serialized the base64 string where the object was serialized.
+     * @return the Object deserialized.
+     * @throws IOException if the deserialization fails.
+     */
+    public static Object deserialize(String serialized) throws IOException {
+        ByteArrayInputStream bais = null;
+        ObjectInputStream in = null;
+        try {
+            bais = new ByteArrayInputStream(Base64.getDecoder().decode(serialized));
+            in = new ObjectInputStream(bais);
+            return in.readObject();
+        } catch (ClassNotFoundException cnfe) {
+            throw new RuntimeException(cnfe);
+        } finally {
+            StreamUtil.close(in);
+            StreamUtil.close(bais);
+        }
+    }
+
+    /**
      * Deserialize an object that was serialized using {@link #serialize(Serializable)}.
      *
      * @param serializedFile the file where the object was serialized.
diff --git a/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java b/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java
index 14ec4ef..618b6bc 100644
--- a/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java
+++ b/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java
@@ -135,6 +135,12 @@
             + "keys parsed and value is be the replacement string.")
     private Map<String, String> mReplacePrefixMap = new LinkedHashMap<String, String>();
 
+    @Option(
+            name = "perfetto-all-metric-prefix",
+            description = "Prefix to be used with the metrics collected from perfetto."
+                    + "This will be applied before any other prefixes to metrics.")
+    private String mAllMetricPrefix = "perfetto";
+
     // Matches 1.73, 1.73E+2
     private Pattern mNumberWithExponentPattern =
             Pattern.compile("[-+]?[0-9]*[\\.]?[0-9]+([eE][-+]?[0-9]+)?");
@@ -222,6 +228,9 @@
                         parsedMetrics.putAll(
                                 filterMetrics(convertPerfettoProtoMessage(builder.build())));
                         replacePrefix(parsedMetrics);
+                        // Generic prefix string is applied to all the metrics parsed from
+                        // perfetto trace file.
+                        replaceAllMetricPrefix(parsedMetrics);
                         break;
                     case binary:
                         TraceMetrics metricProto = null;
@@ -230,6 +239,9 @@
                         parsedMetrics
                                 .putAll(filterMetrics(convertPerfettoProtoMessage(metricProto)));
                         replacePrefix(parsedMetrics);
+                        // Generic prefix string is applied to all the metrics parsed from
+                        // perfetto trace file.
+                        replaceAllMetricPrefix(parsedMetrics);
                         break;
                     case json:
                         CLog.w("JSON perfetto metric file processing not supported.");
@@ -245,6 +257,7 @@
                 FileUtil.recursiveDelete(uncompressedDir);
             }
         }
+
         return parsedMetrics;
     }
 
@@ -279,6 +292,25 @@
     }
 
     /**
+     * Prefix all the metrics key with given string.
+     *
+     * @param processPerfettoMetrics metrics parsed from the perfetto proto file.
+     */
+    private void replaceAllMetricPrefix(Map<String, Metric.Builder> processPerfettoMetrics) {
+        if (mAllMetricPrefix == null || mAllMetricPrefix.isEmpty()) {
+            return;
+        }
+        Map<String, Metric.Builder> finalMetrics = new HashMap<String, Metric.Builder>();
+        for (Map.Entry<String, Metric.Builder> metric : processPerfettoMetrics.entrySet()) {
+            String newKey = String.format("%s_%s", mAllMetricPrefix, metric.getKey());
+            finalMetrics.put(newKey, metric.getValue());
+            CLog.d("Perfetto trace metric: key: %s value: %s", newKey, metric.getValue());
+        }
+        processPerfettoMetrics.clear();
+        processPerfettoMetrics.putAll(finalMetrics);
+    }
+
+    /**
      * Expands the metric proto file as tree structure and converts it into key, value pairs by
      * recursively constructing the key using the message name, proto fields with string values
      * until the numeric proto field is encountered.
diff --git a/test_framework/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
index 199037b..8e3ef8c 100644
--- a/test_framework/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
@@ -87,6 +87,13 @@
 
         cleanUpStagedAndActiveSession(device);
 
+        Set<ApexInfo> activatedApexes = device.getActiveApexes();
+
+        CLog.i("Activated apex packages list before module/train installation:");
+        for (ApexInfo info : activatedApexes) {
+            CLog.i("Activated apex: %s", info.toString());
+        }
+
         List<String> testAppFileNames = getModulesToInstall(testInfo);
         if (testAppFileNames.isEmpty()) {
             CLog.i("No modules are preloaded on the device, so no modules will be installed.");
@@ -114,7 +121,7 @@
             }
         }
 
-        Set<ApexInfo> activatedApexes = device.getActiveApexes();
+        activatedApexes = device.getActiveApexes();
 
         if (activatedApexes.isEmpty()) {
             throw new TargetSetupError(
@@ -122,6 +129,11 @@
                             "Failed to retrieve activated apex on device %s. Empty set returned.",
                             device.getSerialNumber()),
                     device.getDeviceDescriptor());
+        } else {
+            CLog.i("Activated apex packages list after module/train installation:");
+            for (ApexInfo info : activatedApexes) {
+                CLog.i("Activated apex: %s", info.toString());
+            }
         }
 
         List<ApexInfo> failToActivateApex = new ArrayList<ApexInfo>();
@@ -133,10 +145,6 @@
         }
 
         if (!failToActivateApex.isEmpty()) {
-            CLog.i("Activated apex packages list:");
-            for (ApexInfo info : activatedApexes) {
-                CLog.i("Activated apex: %s", info.toString());
-            }
             throw new TargetSetupError(
                     String.format(
                             "Failed to activate %s on device %s.",
diff --git a/test_framework/com/android/tradefed/targetprep/RootTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RootTargetPreparer.java
index 50ddaae..8d6031b 100644
--- a/test_framework/com/android/tradefed/targetprep/RootTargetPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/RootTargetPreparer.java
@@ -15,12 +15,14 @@
  */
 package com.android.tradefed.targetprep;
 
+import com.android.tradefed.command.remote.DeviceDescriptor;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.StubDevice;
 import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
 
 /**
  * Target preparer that performs "adb root" or "adb unroot" based on option "force-root".
@@ -39,6 +41,11 @@
                             + "root during setup.")
     private boolean mForceRoot = true;
 
+    @Option(
+            name = "throw-on-error",
+            description = "Throws TargetSetupError if adb root/unroot fails")
+    private boolean mThrowOnError = true;
+
     @Override
     public void setUp(TestInformation testInfo)
             throws TargetSetupError, BuildError, DeviceNotAvailableException {
@@ -49,9 +56,9 @@
         }
         mWasRoot = device.isAdbRoot();
         if (!mWasRoot && mForceRoot && !device.enableAdbRoot()) {
-            throw new TargetSetupError("Failed to adb root device", device.getDeviceDescriptor());
+            throwOrLog("Failed to adb root device", device.getDeviceDescriptor());
         } else if (mWasRoot && !mForceRoot && !device.disableAdbRoot()) {
-            throw new TargetSetupError("Failed to adb unroot device", device.getDeviceDescriptor());
+            throwOrLog("Failed to adb unroot device", device.getDeviceDescriptor());
         }
     }
 
@@ -68,4 +75,13 @@
             device.enableAdbRoot();
         }
     }
+
+    private void throwOrLog(String message, DeviceDescriptor deviceDescriptor)
+            throws TargetSetupError {
+        if (mThrowOnError) {
+            throw new TargetSetupError(message, deviceDescriptor);
+        } else {
+            CLog.w(message + " " + deviceDescriptor);
+        }
+    }
 }
diff --git a/test_framework/com/android/tradefed/testtype/InstrumentationFileTest.java b/test_framework/com/android/tradefed/testtype/InstrumentationFileTest.java
index 5fbbd76..7c0d7b6 100644
--- a/test_framework/com/android/tradefed/testtype/InstrumentationFileTest.java
+++ b/test_framework/com/android/tradefed/testtype/InstrumentationFileTest.java
@@ -203,7 +203,7 @@
         } finally {
             deleteTestFileFromDevice(mFilePathOnDevice);
             Collection<TestDescription> completedTests =
-                    testTracker.getCurrentRunResults().getCompletedTests();
+                    InstrumentationTest.excludeNonExecuted(testTracker.getCurrentRunResults());
             if (mTests.removeAll(completedTests) && !mTests.isEmpty()) {
                 // re-run remaining tests from file
                 writeTestsToFileAndRun(mTests, testInfo, listener);
diff --git a/test_framework/com/android/tradefed/testtype/InstrumentationListener.java b/test_framework/com/android/tradefed/testtype/InstrumentationListener.java
index 2a96294..baafb76 100644
--- a/test_framework/com/android/tradefed/testtype/InstrumentationListener.java
+++ b/test_framework/com/android/tradefed/testtype/InstrumentationListener.java
@@ -45,6 +45,7 @@
     private Set<TestDescription> mDuplicateTests = new HashSet<>();
     private final Collection<TestDescription> mExpectedTests;
     private boolean mDisableDuplicateCheck = false;
+    private boolean mReportUnexecutedTests = false;
     private ProcessInfo mSystemServerProcess = null;
 
     /**
@@ -68,6 +69,10 @@
         mSystemServerProcess = info;
     }
 
+    public void setReportUnexecutedTests(boolean enable) {
+        mReportUnexecutedTests = enable;
+    }
+
     @Override
     public void testRunStarted(String runName, int testCount) {
         // In case of crash, run will attempt to report with 0
@@ -123,6 +128,18 @@
                                     mDuplicateTests));
             error.setFailureStatus(FailureStatus.TEST_FAILURE);
             super.testRunFailed(error);
+        } else if (mReportUnexecutedTests && mExpectedTests.size() > mTests.size()) {
+            Set<TestDescription> missingTests = new LinkedHashSet<>(mExpectedTests);
+            missingTests.removeAll(mTests);
+            for (TestDescription miss : missingTests) {
+                super.testStarted(miss);
+                FailureDescription failure =
+                        FailureDescription.create(
+                                "test did not run due to instrumentation issue.",
+                                FailureStatus.NOT_EXECUTED);
+                super.testFailed(miss, failure);
+                super.testEnded(miss, new HashMap<String, Metric>());
+            }
         }
         super.testRunEnded(elapsedTime, runMetrics);
     }
diff --git a/test_framework/com/android/tradefed/testtype/InstrumentationTest.java b/test_framework/com/android/tradefed/testtype/InstrumentationTest.java
index 97ae0dd..108f920 100644
--- a/test_framework/com/android/tradefed/testtype/InstrumentationTest.java
+++ b/test_framework/com/android/tradefed/testtype/InstrumentationTest.java
@@ -45,8 +45,10 @@
 import com.android.tradefed.result.CollectingTestListener;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.TestResult;
 import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.result.ddmlib.DefaultRemoteAndroidTestRunner;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.retry.IRetryDecision;
 import com.android.tradefed.retry.RetryStrategy;
 import com.android.tradefed.testtype.coverage.CoverageOptions;
@@ -68,6 +70,7 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
@@ -315,6 +318,12 @@
                             + "a system_server restart.")
     private boolean mEnableSoftRestartCheck = false;
 
+    @Option(
+            name = "report-unexecuted-tests",
+            description =
+                    "Whether or not to enable reporting all unexecuted tests from instrumentation.")
+    private boolean mReportUnexecuted = true;
+
     private IAbi mAbi = null;
 
     private Collection<String> mInstallArgs = new ArrayList<>();
@@ -1053,13 +1062,14 @@
             instrumentationListener.setOriginalSystemServer(
                     getDevice().getProcessByName("system_server"));
         }
+        instrumentationListener.setReportUnexecutedTests(mReportUnexecuted);
         mDevice.runInstrumentationTests(mRunner, instrumentationListener);
         TestRunResult testRun = testTracker.getCurrentRunResults();
         if (testRun.isRunFailure() || !testRun.getCompletedTests().containsAll(expectedTests)) {
             // Don't re-run any completed tests, unless this is a coverage run.
             if (mConfiguration != null
                     && !mConfiguration.getCoverageOptions().isCoverageEnabled()) {
-                expectedTests.removeAll(testTracker.getCurrentRunResults().getCompletedTests());
+                expectedTests.removeAll(excludeNonExecuted(testTracker.getCurrentRunResults()));
                 IRetryDecision decision = mConfiguration.getRetryDecision();
                 if (!RetryStrategy.NO_RETRY.equals(decision.getRetryStrategy())
                         && decision.getMaxRetryCount() > 1) {
@@ -1073,6 +1083,20 @@
         }
     }
 
+    /** Filter out "NOT_EXECUTED" for the purpose of tracking what needs to be rerun. */
+    protected static Set<TestDescription> excludeNonExecuted(TestRunResult results) {
+        Set<TestDescription> completedTest = results.getCompletedTests();
+        for (Entry<TestDescription, TestResult> entry : results.getTestResults().entrySet()) {
+            if (completedTest.contains(entry.getKey()) && entry.getValue().getFailure() != null) {
+                if (FailureStatus.NOT_EXECUTED.equals(
+                        entry.getValue().getFailure().getFailureStatus())) {
+                    completedTest.remove(entry.getKey());
+                }
+            }
+        }
+        return completedTest;
+    }
+
     /**
      * Rerun any <var>mRemainingTests</var>
      *
diff --git a/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java b/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
index cdd11be..1d23bfe 100644
--- a/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
@@ -42,6 +42,8 @@
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.SubprocessTestResultsParser;
 
+import com.google.common.base.Joiner;
+
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -64,6 +66,7 @@
 public class PythonBinaryHostTest implements IRemoteTest, ITestFilterReceiver {
 
     protected static final String ANDROID_SERIAL_VAR = "ANDROID_SERIAL";
+    protected static final String LD_LIBRARY_PATH = "LD_LIBRARY_PATH";
     protected static final String PATH_VAR = "PATH";
     protected static final long PATH_TIMEOUT_MS = 60000L;
 
@@ -75,6 +78,7 @@
 
     private Set<String> mIncludeFilters = new LinkedHashSet<>();
     private Set<String> mExcludeFilters = new LinkedHashSet<>();
+    private String mLdLibraryPath = null;
 
     @Option(name = "par-file-name", description = "The binary names inside the build info to run.")
     private Set<String> mBinaryNames = new HashSet<>();
@@ -174,6 +178,22 @@
     public final void run(TestInformation testInfo, ITestInvocationListener listener)
             throws DeviceNotAvailableException {
         mTestInfo = testInfo;
+        File hostTestDir = mTestInfo.executionFiles().get(FilesKey.HOST_TESTS_DIRECTORY);
+        if (hostTestDir.exists()) {
+            File libDir = new File(hostTestDir, "lib");
+            List<String> ldLibraryPath = new ArrayList<>();
+            if (libDir.exists()) {
+                ldLibraryPath.add(libDir.getAbsolutePath());
+            }
+
+            File lib64Dir = new File(hostTestDir, "lib64");
+            if (lib64Dir.exists()) {
+                ldLibraryPath.add(lib64Dir.getAbsolutePath());
+            }
+            if (!ldLibraryPath.isEmpty()) {
+                mLdLibraryPath = Joiner.on(":").join(ldLibraryPath);
+            }
+        }
         List<File> pythonFilesList = findParFiles();
         for (File pyFile : pythonFilesList) {
             if (!pyFile.exists()) {
@@ -222,6 +242,9 @@
             commandLine.add(mTestInfo.getDevice().getSerialNumber());
         }
 
+        if (mLdLibraryPath != null) {
+            getRunUtil().setEnvVariable(LD_LIBRARY_PATH, mLdLibraryPath);
+        }
         if (mInjectAndroidSerialVar) {
             getRunUtil()
                     .setEnvVariable(ANDROID_SERIAL_VAR, mTestInfo.getDevice().getSerialNumber());
diff --git a/tests/res/util/JUnitXmlParserTest_error2.xml b/tests/res/util/JUnitXmlParserTest_error2.xml
new file mode 100644
index 0000000..f4bbd36
--- /dev/null
+++ b/tests/res/util/JUnitXmlParserTest_error2.xml
@@ -0,0 +1,11 @@
+<testsuites>
+    <testsuite name="normal_integration_tests" tests="1" failures="0" errors="1">
+        <testcase name="normal_integration_tests" status="run" duration="0" time="0">
+            <error message="exited with error code 134"/>
+        </testcase>
+        <system-out>
+        Generated test.log (if the file is not UTF-8, then this may be unreadable):
+        <![CDATA[some logs]]>
+        </system-out>
+    </testsuite>
+</testsuites>
diff --git a/tests/src/com/android/tradefed/build/FileDownloadCacheTest.java b/tests/src/com/android/tradefed/build/FileDownloadCacheTest.java
index d0643c5..6e84b35 100644
--- a/tests/src/com/android/tradefed/build/FileDownloadCacheTest.java
+++ b/tests/src/com/android/tradefed/build/FileDownloadCacheTest.java
@@ -92,6 +92,18 @@
         EasyMock.verify(mMockDownloader);
     }
 
+    @Test
+    public void testFetchRemoteFile_destFile_nullPath() throws Exception {
+        EasyMock.replay(mMockDownloader);
+        try {
+            assertFetchRemoteFile(null, null, null);
+            fail("Should have thrown an exception.");
+        } catch (BuildRetrievalError expected) {
+            assertEquals("remote path was null.", expected.getMessage());
+        }
+        EasyMock.verify(mMockDownloader);
+    }
+
     /**
      * Test {@link FileDownloadCache#fetchRemoteFile(IFileDownloader, String)} when file can be
      * retrieved from cache.
diff --git a/tests/src/com/android/tradefed/device/DeviceManagerTest.java b/tests/src/com/android/tradefed/device/DeviceManagerTest.java
index f81d999..3d1d564 100644
--- a/tests/src/com/android/tradefed/device/DeviceManagerTest.java
+++ b/tests/src/com/android/tradefed/device/DeviceManagerTest.java
@@ -201,6 +201,8 @@
 
         EasyMock.expect(mMockIDevice.getSerialNumber()).andStubReturn(DEVICE_SERIAL);
         EasyMock.expect(mMockStateMonitor.getSerialNumber()).andStubReturn(DEVICE_SERIAL);
+        mMockStateMonitor.waitForDeviceBootloaderStateUpdate();
+        EasyMock.expectLastCall().anyTimes();
         EasyMock.expect(mMockIDevice.isEmulator()).andStubReturn(Boolean.FALSE);
         EasyMock.expect(mMockTestDevice.getMacAddress()).andStubReturn(MAC_ADDRESS);
         EasyMock.expect(mMockTestDevice.getSimState()).andStubReturn(SIM_STATE);
@@ -1046,6 +1048,67 @@
         assertEquals(1, manager.getDeviceList().size());
     }
 
+    /** Ensure that an unavailable device in recovery mode is released properly. */
+    @Test
+    public void testFreeDevice_recovery() {
+        EasyMock.expect(mMockIDevice.isEmulator()).andStubReturn(Boolean.FALSE);
+        EasyMock.expect(mMockIDevice.getState()).andReturn(DeviceState.ONLINE);
+        EasyMock.expect(mMockStateMonitor.waitForDeviceShell(EasyMock.anyLong()))
+                .andReturn(Boolean.TRUE);
+        mMockStateMonitor.setState(TestDeviceState.NOT_AVAILABLE);
+
+        CommandResult stubAdbDevices = new CommandResult(CommandStatus.SUCCESS);
+        stubAdbDevices.setStdout("List of devices attached\nserial\trecovery\n");
+        EasyMock.expect(
+                        mMockRunUtil.runTimedCmd(
+                                EasyMock.anyLong(), EasyMock.eq("adb"), EasyMock.eq("devices")))
+                .andReturn(stubAdbDevices);
+
+        replayMocks();
+        IManagedTestDevice testDevice = new TestDevice(mMockIDevice, mMockStateMonitor, null);
+        DeviceManager manager = createDeviceManagerNoInit();
+        manager.init(
+                null,
+                null,
+                new ManagedTestDeviceFactory(false, null, null) {
+                    @Override
+                    public IManagedTestDevice createDevice(IDevice idevice) {
+                        mMockTestDevice.setIDevice(idevice);
+                        return testDevice;
+                    }
+
+                    @Override
+                    protected CollectingOutputReceiver createOutputReceiver() {
+                        return new CollectingOutputReceiver() {
+                            @Override
+                            public String getOutput() {
+                                return "/system/bin/pm";
+                            }
+                        };
+                    }
+
+                    @Override
+                    public void setFastbootEnabled(boolean enable) {
+                        // ignore
+                    }
+                });
+
+        mDeviceListener.deviceConnected(mMockIDevice);
+
+        IManagedTestDevice device = (IManagedTestDevice) manager.allocateDevice(mDeviceSelections);
+        assertNotNull(device);
+        // Device becomes unavailable
+        device.setDeviceState(TestDeviceState.NOT_AVAILABLE);
+        // A freed 'unavailable' device becomes UNAVAILABLE state
+        manager.freeDevice(device, FreeDeviceState.UNAVAILABLE);
+        // Ensure device cannot be allocated again
+        ITestDevice device2 = manager.allocateDevice(mDeviceSelections);
+        assertNull(device2);
+        verifyMocks();
+        // We still have the device in the list because device is not lost.
+        assertEquals(1, manager.getDeviceList().size());
+    }
+
     /**
      * Test that when freeing an Unavailable device that is not in 'adb devices' we correctly remove
      * it from our tracking list.
diff --git a/tests/src/com/android/tradefed/device/cloud/GceAvdInfoTest.java b/tests/src/com/android/tradefed/device/cloud/GceAvdInfoTest.java
index 79e7c15..35ed79d 100644
--- a/tests/src/com/android/tradefed/device/cloud/GceAvdInfoTest.java
+++ b/tests/src/com/android/tradefed/device/cloud/GceAvdInfoTest.java
@@ -21,11 +21,17 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.targetprep.TargetSetupError;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.json.JSONObject;
+import org.json.JSONArray;
+
+import java.util.Map;
 
 /** Unit tests for {@link GceAvdInfo} */
 @RunWith(JUnit4.class)
@@ -306,4 +312,33 @@
             // expected
         }
     }
+
+    /** Test CF start time metrics are added. */
+    @Test
+    public void testCfStartTimeMetricsAdded() throws Exception {
+        String cuttlefish =
+                " {\n"
+                        + "    \"command\": \"create_cf\",\n"
+                        + "    \"data\": {\n"
+                        + "      \"devices\": [\n"
+                        + "        {\n"
+                        + "          \"ip\": \"34.71.83.182\",\n"
+                        + "          \"instance_name\": \"ins-cf-x86-phone-userdebug\",\n"
+                        + "          \"fetch_artifact_time\": 63.22,\n"
+                        + "          \"gce_create_time\": 23.5,\n"
+                        + "          \"launch_cvd_time\": 226.5\n"
+                        + "        },\n"
+                        + "      ]\n"
+                        + "    },\n"
+                        + "    \"errors\": [],\n"
+                        + "    \"status\": \"SUCCESS\"\n"
+                        + "  }";
+        JSONObject res = new JSONObject(cuttlefish);
+        JSONArray devices = res.getJSONObject("data").getJSONArray("devices");
+        GceAvdInfo.addCfStartTimeMetrics((JSONObject) devices.get(0));
+        Map<String, String> metrics = InvocationMetricLogger.getInvocationMetrics();
+        assertEquals("63220", metrics.get(InvocationMetricKey.CF_FETCH_ARTIFACT_TIME.toString()));
+        assertEquals("23500", metrics.get(InvocationMetricKey.CF_GCE_CREATE_TIME.toString()));
+        assertEquals("226500", metrics.get(InvocationMetricKey.CF_LAUNCH_CVD_TIME.toString()));
+    }
 }
diff --git a/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java b/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
index c6beebd..654055e 100644
--- a/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
+++ b/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
@@ -251,6 +251,7 @@
                 .saveLogData(any(), any(), any());
 
         CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        result.setExitCode(0);
         doReturn(result).when(mMockSandbox).run(any(), any());
 
         doReturn(new BuildInfo()).when(mMockProvider).getBuild();
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
index 6b01fc4..48a2590 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
@@ -51,6 +51,7 @@
 import com.android.tradefed.device.StubDevice;
 import com.android.tradefed.device.TcpDevice;
 import com.android.tradefed.device.TestDeviceOptions;
+import com.android.tradefed.device.TestDeviceState;
 import com.android.tradefed.device.metric.BaseDeviceMetricCollector;
 import com.android.tradefed.device.metric.DeviceMetricData;
 import com.android.tradefed.device.metric.IMetricCollector;
@@ -217,6 +218,8 @@
         EasyMock.expect(mMockDevice.getSerialNumber()).andStubReturn(SERIAL);
         EasyMock.expect(mMockDevice.getIDevice()).andStubReturn(null);
         EasyMock.expect(mMockDevice.getBattery()).andStubReturn(null);
+        EasyMock.expect(mMockDevice.getDeviceState()).andStubReturn(TestDeviceState.NOT_AVAILABLE);
+        mMockDevice.setRecoveryMode(RecoveryMode.AVAILABLE);
         mMockDevice.setRecovery(mMockRecovery);
         mMockDevice.preInvocationSetup(
                 (IBuildInfo) EasyMock.anyObject(), EasyMock.<List<IBuildInfo>>anyObject());
@@ -307,6 +310,12 @@
                     public void registerExecutionFiles(ExecutionFiles executionFiles) {
                         // Empty of purpose
                     }
+
+                    @Override
+                    protected void addInvocationMetric(InvocationMetricKey key, long value) {}
+
+                    @Override
+                    protected void addInvocationMetric(InvocationMetricKey key, String value) {}
                 };
     }
 
@@ -1565,6 +1574,12 @@
                         // Avoid re-entry in the current TF invocation scope for unit tests.
                         return new InvocationScope();
                     }
+
+                    @Override
+                    protected void addInvocationMetric(InvocationMetricKey key, long value) {}
+
+                    @Override
+                    protected void addInvocationMetric(InvocationMetricKey key, String value) {}
                 };
         mMockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
         EasyMock.expect(mMockBuildInfo.getProperties()).andStubReturn(new HashSet<>());
@@ -1645,6 +1660,12 @@
                             // Avoid re-entry in the current TF invocation scope for unit tests.
                             return new InvocationScope();
                         }
+
+                        @Override
+                        protected void addInvocationMetric(InvocationMetricKey key, long value) {}
+
+                        @Override
+                        protected void addInvocationMetric(InvocationMetricKey key, String value) {}
                     };
             mMockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
             IRemoteTest test = EasyMock.createNiceMock(IRemoteTest.class);
@@ -1743,6 +1764,12 @@
                             // Avoid re-entry in the current TF invocation scope for unit tests.
                             return new InvocationScope();
                         }
+
+                        @Override
+                        protected void addInvocationMetric(InvocationMetricKey key, long value) {}
+
+                        @Override
+                        protected void addInvocationMetric(InvocationMetricKey key, String value) {}
                     };
             mMockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
             IRemoteTest test = EasyMock.createNiceMock(IRemoteTest.class);
diff --git a/tests/src/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java b/tests/src/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java
index 71c02fd..d83c343 100644
--- a/tests/src/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java
+++ b/tests/src/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java
@@ -67,6 +67,7 @@
     private static final String KEY_PREFIX_OPTION = "perfetto-prefix-key-field";
     private static final String REGEX_OPTION_VALUE = "perfetto-metric-filter-regex";
     private static final String ALL_METRICS_OPTION = "perfetto-include-all-metrics";
+    private static final String ALL_METRICS_PREFIX_OPTION = "perfetto-all-metric-prefix";
     private static final String REPLACE_REGEX_OPTION = "perfetto-metric-replace-prefix";
     private static final String FILE_FORMAT_OPTION = "trace-processor-output-format";
 
@@ -120,10 +121,10 @@
         Map<String, Metric.Builder> parsedMetrics =
                 mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
 
-        assertMetricsContain(parsedMetrics, "android_startup-startup-1-startup_id", 1);
+        assertMetricsContain(parsedMetrics, "perfetto_android_startup-startup-1-startup_id", 1);
         assertMetricsContain(
                 parsedMetrics,
-                "android_startup-startup-1-package_name-com.google."
+                "perfetto_android_startup-startup-1-package_name-com.google."
                         + "android.apps.nexuslauncher-to_first_frame-dur_ns",
                 36175473);
     }
@@ -152,10 +153,10 @@
 
         assertFalse("Metric key not expected but found",
                 parsedMetrics.containsKey("android_startup-startup-1-startup_id"));
-        assertMetricsContain(parsedMetrics, "newprefix-startup_id", 1);
+        assertMetricsContain(parsedMetrics, "perfetto_newprefix-startup_id", 1);
         assertMetricsContain(
                 parsedMetrics,
-                "newprefix-package_name-com.google."
+                "perfetto_newprefix-package_name-com.google."
                         + "android.apps.nexuslauncher-to_first_frame-dur_ns",
                 36175473);
     }
@@ -180,7 +181,7 @@
         // Test for non startup metrics exists.
         assertMetricsContain(
                 parsedMetrics,
-                "android_mem-process_metrics-process_name-"
+                "perfetto_android_mem-process_metrics-process_name-"
                         + ".dataservices-total_counters-anon_rss-min",
                 27938816);
     }
@@ -201,7 +202,29 @@
                         new TestDescription("class", "test"), new HashMap<>(), testLogs);
         assertMetricsContain(
                 parsedMetrics,
-                "android_mem-process_metrics-process_name-"
+                "perfetto_android_mem-process_metrics-process_name-"
+                        + ".dataservices-total_counters-anon_rss-min",
+                27938816);
+    }
+
+    /** Test custom all metric suffix is applied correctly. */
+    @Test
+    public void testParsingWithAllMetricsPrefix() throws ConfigurationException, IOException {
+        setupPerfettoMetricFile(METRIC_FILE_FORMAT.text, true);
+        mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
+        mOptionSetter.setOptionValue(ALL_METRICS_OPTION, "true");
+        mOptionSetter.setOptionValue(ALL_METRICS_PREFIX_OPTION, "custom_all_prefix");
+        Map<String, LogFile> testLogs = new HashMap<>();
+        testLogs.put(
+                PREFIX_OPTION_VALUE,
+                new LogFile(
+                        perfettoMetricProtoFile.getAbsolutePath(), "some.url", LogDataType.TEXTPB));
+        Map<String, Metric.Builder> parsedMetrics =
+                mProcessor.processTestMetricsAndLogs(
+                        new TestDescription("class", "test"), new HashMap<>(), testLogs);
+        assertMetricsContain(
+                parsedMetrics,
+                "custom_all_prefix_android_mem-process_metrics-process_name-"
                         + ".dataservices-total_counters-anon_rss-min",
                 27938816);
     }
@@ -221,7 +244,7 @@
                 mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
         assertMetricsContain(
                 parsedMetrics,
-                "android_mem-process_metrics-process_name-"
+                "perfetto_android_mem-process_metrics-process_name-"
                         + ".dataservices-total_counters-anon_rss-min",
                 27938816);
     }
@@ -242,10 +265,10 @@
                         perfettoMetricProtoFile.getAbsolutePath(), "some.url", LogDataType.TEXTPB));
         Map<String, Metric.Builder> parsedMetrics =
                 mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
-        assertMetricsContain(parsedMetrics, "android_startup-startup-startup_id", 2);
+        assertMetricsContain(parsedMetrics, "perfetto_android_startup-startup-startup_id", 2);
         assertMetricsContain(
                 parsedMetrics,
-                "android_startup-startup-package_name-com.google."
+                "perfetto_android_startup-startup-package_name-com.google."
                         + "android.apps.nexuslauncher-to_first_frame-dur_ns",
                 53102401);
     }
@@ -268,16 +291,16 @@
         Map<String, Metric.Builder> parsedMetrics =
                 mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
 
-        assertMetricsContain(parsedMetrics, "android_startup-startup-1-startup_id", 1);
+        assertMetricsContain(parsedMetrics, "perfetto_android_startup-startup-1-startup_id", 1);
         assertMetricsContain(
                 parsedMetrics,
-                "android_startup-startup-1-package_name-com.google."
+                "perfetto_android_startup-startup-1-package_name-com.google."
                         + "android.apps.nexuslauncher-to_first_frame-dur_ns",
                 36175473);
-        assertMetricsContain(parsedMetrics, "android_startup-startup-2-startup_id", 2);
+        assertMetricsContain(parsedMetrics, "perfetto_android_startup-startup-2-startup_id", 2);
         assertMetricsContain(
                 parsedMetrics,
-                "android_startup-startup-2-package_name-com.google."
+                "perfetto_android_startup-startup-2-package_name-com.google."
                         + "android.apps.nexuslauncher-to_first_frame-dur_ns",
                 53102401);
     }
@@ -301,7 +324,7 @@
                 mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
 
         assertMetricsContain(parsedMetrics,
-                "android_hwui_metric-process_info-process_name-com.android.systemui-all_mem_min",
+                "perfetto_android_hwui_metric-process_info-process_name-com.android.systemui-all_mem_min",
                 15120269);
 
     }
@@ -323,7 +346,7 @@
                 mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
         assertMetricsContain(
                 parsedMetrics,
-                "android_mem-process_metrics-process_name-"
+                "perfetto_android_mem-process_metrics-process_name-"
                         + ".dataservices-total_counters-anon_rss-min",
                 27938816);
     }
@@ -366,7 +389,7 @@
                 mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
         assertMetricsContain(
                 parsedMetrics,
-                "android_mem-process_metrics-process_name-"
+                "perfetto_android_mem-process_metrics-process_name-"
                         + ".dataservices-total_counters-anon_rss-min",
                 27938816);
     }
diff --git a/tests/src/com/android/tradefed/result/ConsoleResultReporterTest.java b/tests/src/com/android/tradefed/result/ConsoleResultReporterTest.java
index f1290f8..fd0a169 100644
--- a/tests/src/com/android/tradefed/result/ConsoleResultReporterTest.java
+++ b/tests/src/com/android/tradefed/result/ConsoleResultReporterTest.java
@@ -88,7 +88,7 @@
     public void testSummary() {
         mResultReporter.testResult(mTest, createTestResult(TestStatus.PASSED));
         mResultReporter.invocationEnded(0);
-        Truth.assertThat(mOutput.toString()).contains("1 Tests [1 Passed ");
+        Truth.assertThat(mOutput.toString()).contains("1 Tests [1 Passed]");
     }
 
     @Test
diff --git a/tests/src/com/android/tradefed/result/proto/ProtoResultParserTest.java b/tests/src/com/android/tradefed/result/proto/ProtoResultParserTest.java
index 517c0a0..eabcab2 100644
--- a/tests/src/com/android/tradefed/result/proto/ProtoResultParserTest.java
+++ b/tests/src/com/android/tradefed/result/proto/ProtoResultParserTest.java
@@ -17,6 +17,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tradefed.build.BuildInfo;
 import com.android.tradefed.config.ConfigurationDef;
@@ -166,7 +167,8 @@
         mMockListener.logAssociation(
                 EasyMock.eq("subprocess-invocation_log1"), EasyMock.anyObject());
         // Invocation failure is replayed
-        mMockListener.invocationFailed(EasyMock.anyObject());
+        Capture<Throwable> captureInvocFailure = new Capture<>();
+        mMockListener.invocationFailed(EasyMock.capture(captureInvocFailure));
         mMockListener.invocationEnded(500L);
 
         EasyMock.replay(mMockListener);
@@ -225,6 +227,9 @@
         assertEquals(logFile.getType(), capturedFile.getType());
         assertEquals(logFile.getSize(), capturedFile.getSize());
 
+        Throwable invocFailureCaptured = captureInvocFailure.getValue();
+        assertTrue(invocFailureCaptured instanceof RuntimeException);
+
         // Check Context at the end
         assertEquals(
                 "build_value", context.getBuildInfos().get(0).getBuildAttributes().get(TEST_KEY));
diff --git a/tests/src/com/android/tradefed/result/proto/StreamProtoResultReporterTest.java b/tests/src/com/android/tradefed/result/proto/StreamProtoResultReporterTest.java
index 007df56..64f2eac 100644
--- a/tests/src/com/android/tradefed/result/proto/StreamProtoResultReporterTest.java
+++ b/tests/src/com/android/tradefed/result/proto/StreamProtoResultReporterTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.tradefed.result.proto;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
 import com.android.tradefed.config.ConfigurationDescriptor;
@@ -30,6 +31,7 @@
 import com.android.tradefed.testtype.suite.ModuleDefinition;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
+import org.easymock.Capture;
 import org.easymock.EasyMock;
 import org.junit.Before;
 import org.junit.Test;
@@ -123,6 +125,29 @@
         assertNull(receiver.getError());
     }
 
+    /** Once the join receiver is done, we don't parse any more events. */
+    @Test
+    public void testStream_stopParsing() throws Exception {
+        StreamProtoReceiver receiver =
+                new StreamProtoReceiver(mMockListener, mMainInvocationContext, true);
+        OptionSetter setter = new OptionSetter(mReporter);
+        try {
+            setter.setOptionValue(
+                    "proto-report-port", Integer.toString(receiver.getSocketServerPort()));
+            // No calls on the mocks
+            EasyMock.replay(mMockListener);
+            // If we join, then we will stop parsing events
+            receiver.joinReceiver(100);
+            mReporter.invocationStarted(mInvocationContext);
+            // Invocation ends
+            mReporter.invocationEnded(500L);
+        } finally {
+            receiver.close();
+        }
+        EasyMock.verify(mMockListener);
+        assertNull(receiver.getError());
+    }
+
     @Test
     public void testStream_noInvocationReporting() throws Exception {
         StreamProtoReceiver receiver =
@@ -188,6 +213,42 @@
         assertNull(receiver.getError());
     }
 
+    @Test
+    public void testStream_incompleteModule() throws Exception {
+        StreamProtoReceiver receiver =
+                new StreamProtoReceiver(mMockListener, mMainInvocationContext, true);
+        OptionSetter setter = new OptionSetter(mReporter);
+        Capture<FailureDescription> capture = new Capture<>();
+        try {
+            setter.setOptionValue(
+                    "proto-report-port", Integer.toString(receiver.getSocketServerPort()));
+            // Verify mocks
+            mMockListener.invocationStarted(EasyMock.anyObject());
+
+            mMockListener.testModuleStarted(EasyMock.anyObject());
+            mMockListener.testRunStarted(EasyMock.eq("arm64 module1"), EasyMock.eq(0));
+            mMockListener.testRunFailed(EasyMock.capture(capture));
+            mMockListener.testRunEnded(
+                    EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
+            mMockListener.testModuleEnded();
+
+            EasyMock.replay(mMockListener);
+            mReporter.invocationStarted(mInvocationContext);
+            // Run modules
+            mReporter.testModuleStarted(createModuleContext("arm64 module1"));
+            // It stops unexpectedly
+        } finally {
+            receiver.joinReceiver(2000);
+            receiver.close();
+            receiver.completeModuleEvents();
+        }
+        EasyMock.verify(mMockListener);
+        assertNull(receiver.getError());
+        assertEquals(
+                "Module was interrupted after starting, results are incomplete.",
+                capture.getValue().getErrorMessage());
+    }
+
     /** Helper to create a module context. */
     private IInvocationContext createModuleContext(String moduleId) {
         IInvocationContext context = new InvocationContext();
diff --git a/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
index bd1f785..83cd35b 100644
--- a/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
@@ -232,7 +232,7 @@
         mockSuccessfulInstallPackageAndReboot(mFakeApex);
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME", 1));
-        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(3);
         Set<String> installableModules = new HashSet<>();
         installableModules.add(APEX_PACKAGE_NAME);
         EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
@@ -253,7 +253,7 @@
         mockSuccessfulInstallPackageAndReboot(mFakeApex);
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME", 1));
-        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(3);
         Set<String> installableModules = new HashSet<>();
         installableModules.add(APEX_PACKAGE_NAME);
         EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
@@ -281,7 +281,7 @@
         mockSuccessfulInstallPackageAndReboot(mFakeApex);
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME", 1));
-        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(3);
         Set<String> installableModules = new HashSet<>();
         installableModules.add(APEX_PACKAGE_NAME);
         EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
@@ -308,7 +308,7 @@
         mMockDevice.reboot();
         EasyMock.expectLastCall();
         mockSuccessfulInstallPackageAndReboot(mFakeApex);
-        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(new HashSet<ApexInfo>()).times(2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(new HashSet<ApexInfo>()).times(3);
         Set<String> installableModules = new HashSet<>();
         installableModules.add(APEX_PACKAGE_NAME);
         EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
@@ -345,7 +345,7 @@
         mockSuccessfulInstallPackageAndReboot(mFakeApex);
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME_TO_FAIL", 1));
-        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(3);
         Set<String> installableModules = new HashSet<>();
         installableModules.add(APEX_PACKAGE_NAME);
         EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
@@ -391,6 +391,7 @@
                 .andReturn(null)
                 .once();
         EasyMock.expect(mMockDevice.uninstallPackage(APK_PACKAGE_NAME)).andReturn(null).once();
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(new HashSet<ApexInfo>()).times(1);
         Set<String> installableModules = new HashSet<>();
         installableModules.add(APK_PACKAGE_NAME);
         EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
@@ -425,6 +426,7 @@
         mockSuccessfulInstallMultiApkWithoutReboot(apks);
         EasyMock.expect(mMockDevice.uninstallPackage(APK_PACKAGE_NAME)).andReturn(null).once();
         EasyMock.expect(mMockDevice.uninstallPackage(APK2_PACKAGE_NAME)).andReturn(null).once();
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(new HashSet<ApexInfo>()).times(1);
         Set<String> installableModules = new HashSet<>();
         installableModules.add(APK_PACKAGE_NAME);
         installableModules.add(APK2_PACKAGE_NAME);
@@ -470,6 +472,7 @@
         EasyMock.expect(mMockDevice.uninstallPackage(PERSISTENT_APK_PACKAGE_NAME))
                 .andReturn(null)
                 .once();
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(new HashSet<ApexInfo>()).times(1);
         Set<String> installableModules = new HashSet<>();
         installableModules.add(APK_PACKAGE_NAME);
         installableModules.add(APK2_PACKAGE_NAME);
@@ -547,6 +550,9 @@
             EasyMock.expect(mMockDevice.uninstallPackage(SPLIT_APK_PACKAGE_NAME))
                 .andReturn(null)
                 .once();
+            EasyMock.expect(mMockDevice.getActiveApexes())
+                    .andReturn(new HashSet<ApexInfo>())
+                    .times(1);
             Set<String> installableModules = new HashSet<>();
             installableModules.add(APK_PACKAGE_NAME);
             installableModules.add(SPLIT_APK_PACKAGE_NAME);
@@ -596,7 +602,7 @@
         mockSuccessfulInstallPackageAndReboot(mFakeApex);
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME", 1));
-        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(3);
         mMockDevice.reboot();
         EasyMock.expectLastCall();
         Set<String> installableModules = new HashSet<>();
@@ -645,7 +651,7 @@
         mockSuccessfulInstallMultiPackageAndReboot();
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME", 1));
-        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(3);
         EasyMock.expect(mMockDevice.uninstallPackage(APK_PACKAGE_NAME)).andReturn(null).once();
         mMockDevice.reboot();
         EasyMock.expectLastCall();
@@ -742,7 +748,7 @@
             mMockDevice.reboot();
             Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
             activatedApex.add(new ApexInfo(SPLIT_APEX_PACKAGE_NAME, 1));
-            EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(2);
+            EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(3);
             EasyMock.expect(mMockDevice.uninstallPackage(SPLIT_APK_PACKAGE_NAME))
                 .andReturn(null)
                 .once();
@@ -845,7 +851,7 @@
 
             Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
             activatedApex.add(new ApexInfo(SPLIT_APEX_PACKAGE_NAME, 1));
-            EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(1);
+            EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(2);
             EasyMock.expect(mMockDevice.uninstallPackage(SPLIT_APK_PACKAGE_NAME))
                     .andReturn(null)
                     .once();
@@ -947,6 +953,7 @@
         EasyMock.expect(mMockDevice.executeShellV2Command("ls " + APEX_DATA_DIR)).andReturn(res);
         EasyMock.expect(mMockDevice.executeShellV2Command("ls " + SESSION_DATA_DIR)).andReturn(res);
         EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(new HashSet<ApexInfo>()).times(1);
         mMockDevice.reboot();
         EasyMock.expectLastCall();
 
@@ -977,7 +984,7 @@
         mockSuccessfulInstallMultiPackageAndReboot();
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME", 1));
-        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(3);
         EasyMock.expect(mMockDevice.uninstallPackage(APK_PACKAGE_NAME)).andReturn(null).once();
         mMockDevice.reboot();
         EasyMock.expectLastCall();
@@ -1037,6 +1044,7 @@
                 .andReturn(null)
                 .once();
         EasyMock.expect(mMockDevice.uninstallPackage(APK_PACKAGE_NAME)).andReturn(null).once();
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(1);
 
         EasyMock.replay(mMockBuildInfo, mMockDevice);
         mInstallApexModuleTargetPreparer.setUp(mTestInfo);
diff --git a/tests/src/com/android/tradefed/targetprep/RootTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/RootTargetPreparerTest.java
index e28d420..22c74cc 100644
--- a/tests/src/com/android/tradefed/targetprep/RootTargetPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/RootTargetPreparerTest.java
@@ -124,4 +124,36 @@
 
         mRootTargetPreparer.setUp(mTestInfo);
     }
+
+    @Test
+    public void testSetUpFail_forceRoot_ignoresFailure() throws Exception {
+        OptionSetter setter = new OptionSetter(mRootTargetPreparer);
+        setter.setOptionValue("throw-on-error", "false");
+
+        EasyMock.expect(mMockDevice.isAdbRoot()).andReturn(false).once();
+        EasyMock.expect(mMockDevice.enableAdbRoot()).andReturn(false).once();
+        EasyMock.expect(mMockDevice.getDeviceDescriptor()).andReturn(null).once();
+        EasyMock.expect(mMockDevice.disableAdbRoot()).andReturn(true).once();
+        EasyMock.replay(mMockDevice, mMockBuildInfo);
+
+        mRootTargetPreparer.setUp(mTestInfo);
+        mRootTargetPreparer.tearDown(mTestInfo, null);
+    }
+
+    @Test
+    public void testSetUpFail_forceUnroot_ignoresFailure() throws Exception {
+        OptionSetter setter = new OptionSetter(mRootTargetPreparer);
+        setter.setOptionValue("force-root", "false");
+        setter.setOptionValue("throw-on-error", "false");
+
+        EasyMock.expect(mMockDevice.isAdbRoot()).andReturn(true).once();
+        EasyMock.expect(mMockDevice.disableAdbRoot()).andReturn(false).once();
+        EasyMock.expect(mMockDevice.getDeviceDescriptor()).andReturn(null).once();
+        EasyMock.expect(mMockDevice.enableAdbRoot()).andReturn(true).once();
+        EasyMock.replay(mMockDevice, mMockBuildInfo);
+
+        mRootTargetPreparer.setUp(mTestInfo);
+        mRootTargetPreparer.tearDown(mTestInfo, null);
+        EasyMock.verify(mMockDevice, mMockBuildInfo);
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java
index 09c5ef4..46e4b7b 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java
@@ -31,6 +31,7 @@
 import com.android.tradefed.invoker.InvocationContext;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.FailureDescription;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.ITestLifeCycleReceiver;
 import com.android.tradefed.result.TestDescription;
@@ -262,6 +263,7 @@
     @Test
     public void testRun_serialReRunOfTwoFailedToCompleteTests()
             throws DeviceNotAvailableException, ConfigurationException {
+        mMockListener = EasyMock.createStrictMock(ITestInvocationListener.class);
         final Collection<TestDescription> testsList = new ArrayList<>(1);
         final TestDescription test1 = new TestDescription("ClassFoo1", "methodBar1");
         final TestDescription test2 = new TestDescription("ClassFoo2", "methodBar2");
@@ -351,6 +353,10 @@
         mMockListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
         mMockListener.testEnded(
                 EasyMock.eq(test1), EasyMock.anyLong(), EasyMock.eq(new HashMap<String, Metric>()));
+        mMockListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mMockListener.testFailed(EasyMock.eq(test2), EasyMock.<FailureDescription>anyObject());
+        mMockListener.testEnded(
+                EasyMock.eq(test2), EasyMock.anyLong(), EasyMock.eq(new HashMap<String, Metric>()));
         mMockListener.testRunEnded(EasyMock.anyLong(), EasyMock.eq(new HashMap<String, Metric>()));
         // first serial re-run:
         mMockListener.testRunStarted(TEST_PACKAGE_VALUE, 0, 1);
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
index c633824..1c5e61a 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
@@ -465,6 +465,9 @@
         inOrder.verify(mMockListener)
                 .testRunFailed(
                         FailureDescription.create(RUN_ERROR_MSG, FailureStatus.TEST_FAILURE));
+        inOrder.verify(mMockListener).testStarted(eq(TEST2), anyLong());
+        inOrder.verify(mMockListener).testFailed(eq(TEST2), (FailureDescription) any());
+        inOrder.verify(mMockListener).testEnded(eq(TEST2), anyLong(), eq(EMPTY_STRING_MAP));
         inOrder.verify(mMockListener).testRunEnded(1, EMPTY_STRING_MAP);
 
         inOrder.verify(mMockListener).testRunStarted(TEST_PACKAGE_VALUE, 0, 1);
diff --git a/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java b/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
index f9cf4e1..2c32f13 100644
--- a/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
@@ -101,6 +101,7 @@
         mMockRunUtil.setEnvVariable(PythonBinaryHostTest.ANDROID_SERIAL_VAR, "SERIAL");
 
         mPythonBinary = FileUtil.createTempFile("python-dir", "");
+        mTestInfo.executionFiles().put(FilesKey.HOST_TESTS_DIRECTORY, new File("/path-not-exist"));
     }
 
     @After
@@ -312,6 +313,7 @@
     public void testRun_withAdbPath() throws Exception {
         mTestInfo.executionFiles().put(FilesKey.ADB_BINARY, new File("/test/adb"));
         File binary = FileUtil.createTempFile("python-dir", "");
+
         try {
             OptionSetter setter = new OptionSetter(mTest);
             setter.setOptionValue("python-binaries", binary.getAbsolutePath());
@@ -344,6 +346,51 @@
         }
     }
 
+    /** Test running the python tests when shared lib is available in HOST_TESTS_DIRECTORY. */
+    @Test
+    public void testRun_withSharedLib() throws Exception {
+        File hostTestsDir = FileUtil.createTempDir("host-test-cases");
+        mTestInfo.executionFiles().put(FilesKey.HOST_TESTS_DIRECTORY, hostTestsDir);
+        File binary = FileUtil.createTempFile("python-dir", "", hostTestsDir);
+        File lib = new File(hostTestsDir, "lib");
+        lib.mkdirs();
+        File lib64 = new File(hostTestsDir, "lib64");
+        lib64.mkdirs();
+
+        try {
+            OptionSetter setter = new OptionSetter(mTest);
+            setter.setOptionValue("python-binaries", binary.getAbsolutePath());
+            mMockRunUtil.setEnvVariable(
+                    PythonBinaryHostTest.LD_LIBRARY_PATH,
+                    lib.getAbsolutePath() + ":" + lib64.getAbsolutePath());
+            expectedAdbPath(mFakeAdb);
+
+            CommandResult res = new CommandResult();
+            res.setStatus(CommandStatus.SUCCESS);
+            res.setStderr("TEST_RUN_STARTED {\"testCount\": 5, \"runName\": \"TestSuite\"}");
+            EasyMock.expect(
+                            mMockRunUtil.runTimedCmd(
+                                    EasyMock.anyLong(), EasyMock.eq(binary.getAbsolutePath())))
+                    .andReturn(res);
+            mMockListener.testRunStarted(
+                    EasyMock.eq(binary.getName()),
+                    EasyMock.eq(5),
+                    EasyMock.eq(0),
+                    EasyMock.anyLong());
+            mMockListener.testLog(
+                    EasyMock.eq(binary.getName() + "-stderr"),
+                    EasyMock.eq(LogDataType.TEXT),
+                    EasyMock.anyObject());
+            EasyMock.expect(mMockDevice.getIDevice()).andReturn(new StubDevice("serial"));
+
+            EasyMock.replay(mMockRunUtil, mMockBuildInfo, mMockListener, mMockDevice);
+            mTest.run(mTestInfo, mMockListener);
+            EasyMock.verify(mMockRunUtil, mMockBuildInfo, mMockListener, mMockDevice);
+        } finally {
+            FileUtil.recursiveDelete(hostTestsDir);
+        }
+    }
+
     /**
      * If the binary returns an exception status, we should throw a runtime exception since
      * something went wrong with the binary setup.
diff --git a/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java b/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
index a32553a..0e56621 100644
--- a/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
@@ -309,6 +309,43 @@
         assertTrue(configMap.containsKey("armeabi-v7a suite/stub1"));
     }
 
+    /**
+     * Test for {@link BaseTestSuite#loadTests()} implementation, for configuration with include
+     * filter set to a non-existent test.
+     */
+    @Test
+    public void testLoadTests_emptyFilter() throws Exception {
+        OptionSetter setter = new OptionSetter(mRunner);
+        setter.setOptionValue("include-filter", "Doesntexist");
+        setter.setOptionValue("suite-config-prefix", "suite");
+        setter.setOptionValue("run-suite-tag", "example-suite");
+        LinkedHashMap<String, IConfiguration> configMap = mRunner.loadTests();
+        assertEquals(0, configMap.size());
+    }
+
+    /**
+     * Test for {@link BaseTestSuite#loadTests()} implementation, for configuration with include
+     * filter set to a non-existent test and enabled failure on empty test set.
+     */
+    @Test
+    public void testLoadTests_emptyFilterWithFailOnEmpty() throws Exception {
+        OptionSetter setter = new OptionSetter(mRunner);
+        setter.setOptionValue("include-filter", "Doesntexist");
+        setter.setOptionValue("fail-on-everything-filtered", "true");
+        setter.setOptionValue("suite-config-prefix", "suite");
+        setter.setOptionValue("run-suite-tag", "example-suite");
+        try {
+            mRunner.loadTests();
+            fail("Should have thrown exception");
+        } catch (IllegalStateException ex) {
+            assertEquals(
+                    "Include filter '{arm64-v8a Doesntexist=[Doesntexist], armeabi-v7a "
+                            + "Doesntexist=[Doesntexist]}' was specified but resulted in "
+                            + "an empty test set.",
+                    ex.getMessage());
+        }
+    }
+
     /** Test that when splitting, the instance of the implementation is used. */
     @Test
     public void testSplit() throws Exception {
diff --git a/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java b/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
index 7430069..3a4ff57 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
@@ -16,6 +16,8 @@
 package com.android.tradefed.testtype.suite;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -358,6 +360,51 @@
         }
     }
 
+    @Test
+    public void testCreateModule() {
+        IConfiguration config = new Configuration("", "");
+        ConfigurationDescriptor descriptor = config.getConfigurationDescription();
+        descriptor.setAbi(new Abi("armeabi-v7a", "32"));
+        descriptor.addMetadata(ITestSuite.PARAMETER_KEY, Arrays.asList("instant_app", "multi_abi"));
+        mModule =
+                new ModuleDefinition(
+                        MODULE_NAME,
+                        mTestList,
+                        mMapDeviceTargetPreparer,
+                        mMultiTargetPrepList,
+                        config);
+        assertNotNull(mModule.getModuleInvocationContext());
+        IInvocationContext moduleContext = mModule.getModuleInvocationContext();
+        assertNull(moduleContext.getAttributes().get(ModuleDefinition.MODULE_PARAMETERIZATION));
+    }
+
+    @Test
+    public void testCreateModule_withParams() {
+        IConfiguration config = new Configuration("", "");
+        ConfigurationDescriptor descriptor = config.getConfigurationDescription();
+        descriptor.setAbi(new Abi("armeabi-v7a", "32"));
+        descriptor.addMetadata(
+                ConfigurationDescriptor.ACTIVE_PARAMETER_KEY, Arrays.asList("instant"));
+        mModule =
+                new ModuleDefinition(
+                        MODULE_NAME,
+                        mTestList,
+                        mMapDeviceTargetPreparer,
+                        mMultiTargetPrepList,
+                        config);
+        assertNotNull(mModule.getModuleInvocationContext());
+        IInvocationContext moduleContext = mModule.getModuleInvocationContext();
+        assertEquals(
+                1,
+                moduleContext.getAttributes().get(ModuleDefinition.MODULE_PARAMETERIZATION).size());
+        assertEquals(
+                "instant",
+                moduleContext
+                        .getAttributes()
+                        .getUniqueMap()
+                        .get(ModuleDefinition.MODULE_PARAMETERIZATION));
+    }
+
     /**
      * Test that {@link ModuleDefinition#run(TestInformation, ITestInvocationListener)} is properly
      * going through the execution flow.
@@ -506,7 +553,7 @@
     public void testParseTokens() throws Exception {
         Configuration config = new Configuration("", "");
         ConfigurationDescriptor descriptor = config.getConfigurationDescription();
-        descriptor.addMetaData(ITestSuite.TOKEN_KEY, Arrays.asList("SIM_CARD"));
+        descriptor.addMetadata(ITestSuite.TOKEN_KEY, Arrays.asList("SIM_CARD"));
         mModule =
                 new ModuleDefinition(
                         MODULE_NAME,
diff --git a/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java b/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
index 3df2d4b..921f675 100644
--- a/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
@@ -311,11 +311,17 @@
         // Ensure that appropriate metadata are set on the module config descriptor
         ConfigurationDescriptor descriptor = instantModule.getConfigurationDescription();
         assertEquals(
-                "instant_app",
+                1,
+                descriptor
+                        .getAllMetaData()
+                        .get(ConfigurationDescriptor.ACTIVE_PARAMETER_KEY)
+                        .size());
+        assertEquals(
+                "instant",
                 descriptor
                         .getAllMetaData()
                         .getUniqueMap()
-                        .get(ConfigurationDescriptor.PARAMETER_KEY));
+                        .get(ConfigurationDescriptor.ACTIVE_PARAMETER_KEY));
         assertEquals("armeabi-v7a", descriptor.getAbi().getName());
     }
 
diff --git a/tests/src/com/android/tradefed/util/GCSFileDownloaderFuncTest.java b/tests/src/com/android/tradefed/util/GCSFileDownloaderFuncTest.java
index c0c5d05..8d97efb 100644
--- a/tests/src/com/android/tradefed/util/GCSFileDownloaderFuncTest.java
+++ b/tests/src/com/android/tradefed/util/GCSFileDownloaderFuncTest.java
@@ -102,6 +102,8 @@
         createFile(mStorage, FILE_CONTENT, BUCKET_NAME, mRemoteRoot, FILE_NAME1);
         createFile(mStorage, FILE_NAME2, BUCKET_NAME, mRemoteRoot, FOLDER_NAME1, FILE_NAME2);
         createFile(mStorage, FILE_NAME3, BUCKET_NAME, mRemoteRoot, FOLDER_NAME1, FILE_NAME3);
+        // Create a special case condition where folder name is also a file name.
+        createFile(mStorage, FILE_NAME3, BUCKET_NAME, mRemoteRoot, FOLDER_NAME1, FOLDER_NAME2);
         createFile(
                 mStorage,
                 FILE_NAME4,
@@ -230,7 +232,7 @@
 
     private void checkDownloadedFolder(File localFile) throws Exception {
         Assert.assertTrue(localFile.isDirectory());
-        Assert.assertEquals(3, localFile.list().length);
+        Assert.assertEquals(4, localFile.list().length);
         for (String filename : localFile.list()) {
             if (filename.equals(FILE_NAME2)) {
                 Assert.assertEquals(
@@ -242,7 +244,7 @@
                         FILE_NAME3,
                         FileUtil.readStringFromFile(
                                 new File(localFile.getAbsolutePath(), filename)));
-            } else if (filename.equals(FOLDER_NAME2)) {
+            } else if (filename.equals(FOLDER_NAME2 + "_folder")) {
                 File subFolder = new File(localFile.getAbsolutePath(), filename);
                 Assert.assertTrue(subFolder.isDirectory());
                 Assert.assertEquals(1, subFolder.list().length);
@@ -250,6 +252,9 @@
                         FILE_NAME4,
                         FileUtil.readStringFromFile(
                                 new File(subFolder.getAbsolutePath(), subFolder.list()[0])));
+            } else if (filename.equals(FOLDER_NAME2)) {
+                File fileWithFolderName = new File(localFile.getAbsolutePath(), filename);
+                Assert.assertTrue(fileWithFolderName.isFile());
             } else {
                 Assert.assertTrue(String.format("Unknonwn file %s", filename), false);
             }
diff --git a/tests/src/com/android/tradefed/util/JUnitXmlParserTest.java b/tests/src/com/android/tradefed/util/JUnitXmlParserTest.java
index 4eeff23..cf7827d 100644
--- a/tests/src/com/android/tradefed/util/JUnitXmlParserTest.java
+++ b/tests/src/com/android/tradefed/util/JUnitXmlParserTest.java
@@ -39,6 +39,7 @@
 public class JUnitXmlParserTest {
     private static final String TEST_PARSE_FILE = "JUnitXmlParserTest_testParse.xml";
     private static final String TEST_PARSE_FILE2 = "JUnitXmlParserTest_error.xml";
+    private static final String TEST_PARSE_FILE3 = "JUnitXmlParserTest_error2.xml";
     private static final String BAZEL_SH_TEST_XML = "JUnitXmlParserTest_bazelShTest.xml";
 
     private ITestInvocationListener mMockListener;
@@ -100,7 +101,9 @@
         mMockListener.testStarted(test3);
         mMockListener.testFailed(
                 EasyMock.eq(test3),
-                EasyMock.eq("java.lang.NullPointerException\n    at FailTest.testFail:65\n        "));
+                EasyMock.eq(
+                        "error message. java.lang.NullPointerException\n    "
+                                + "at FailTest.testFail:65\n        "));
         mMockListener.testEnded(test3, new HashMap<String, Metric>());
 
         mMockListener.testRunEnded(918686L, new HashMap<String, Metric>());
@@ -109,6 +112,19 @@
         EasyMock.verify(mMockListener);
     }
 
+    @Test
+    public void testParseError_format() throws ParseException {
+        mMockListener.testRunStarted("normal_integration_tests", 1);
+        TestDescription test1 = new TestDescription("JUnitXmlParser", "normal_integration_tests");
+        mMockListener.testStarted(test1);
+        mMockListener.testFailed(EasyMock.eq(test1), EasyMock.eq("exited with error code 134. "));
+        mMockListener.testEnded(test1, new HashMap<String, Metric>());
+        mMockListener.testRunEnded(0L, new HashMap<String, Metric>());
+        EasyMock.replay(mMockListener);
+        new JUnitXmlParser(mMockListener).parse(extractTestXml(TEST_PARSE_FILE3));
+        EasyMock.verify(mMockListener);
+    }
+
     /** Test parsing the XML from an sh_test rule in Bazel. */
     @Test
     public void testParseBazelShTestXml() throws ParseException {
diff --git a/tests/src/com/android/tradefed/util/LocalRunInstructionBuilderTest.java b/tests/src/com/android/tradefed/util/LocalRunInstructionBuilderTest.java
index 4a9d110..e861154 100644
--- a/tests/src/com/android/tradefed/util/LocalRunInstructionBuilderTest.java
+++ b/tests/src/com/android/tradefed/util/LocalRunInstructionBuilderTest.java
@@ -110,7 +110,7 @@
         ConfigurationDescriptor configDescriptor = new ConfigurationDescriptor();
         configDescriptor.setAbi(new Abi(ABI_NAME, "32"));
         configDescriptor.setModuleName(OPTION_SOURCE);
-        configDescriptor.addMetadata(ConfigurationDescriptor.PARAMETER_KEY, "instant");
+        configDescriptor.addMetadata(ConfigurationDescriptor.ACTIVE_PARAMETER_KEY, "instant");
         String instruction =
                 LocalRunInstructionBuilder.getInstruction(
                         configDescriptor, LocalTestRunner.ATEST, null);
diff --git a/tests/src/com/android/tradefed/util/SerializationUtilTest.java b/tests/src/com/android/tradefed/util/SerializationUtilTest.java
index aab84cb..08a2f4f 100644
--- a/tests/src/com/android/tradefed/util/SerializationUtilTest.java
+++ b/tests/src/com/android/tradefed/util/SerializationUtilTest.java
@@ -15,7 +15,9 @@
  */
 package com.android.tradefed.util;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.android.tradefed.build.BuildInfo;
 import com.android.tradefed.build.BuildSerializedVersion;
@@ -48,6 +50,15 @@
         }
     }
 
+    @Test
+    public void testSerialize_DeserializeString() throws Exception {
+        RuntimeException e = new RuntimeException("test");
+        String serializedException = SerializationUtil.serializeToString(e);
+        Object o = SerializationUtil.deserialize(serializedException);
+        assertTrue(o instanceof RuntimeException);
+        assertEquals("test", ((RuntimeException) o).getMessage());
+    }
+
     /** Tests that serialization and deserialization creates a similar object from the original. */
     @Test
     public void testSerialize_Deserialize() throws Exception {