Merge "Snap for 6877830 from 93374d31f2ad53f028c2614f492d429e93010691 to sdk-release" into sdk-release
diff --git a/Android.bp b/Android.bp
index 4d6f01b..fb8090f 100644
--- a/Android.bp
+++ b/Android.bp
@@ -48,6 +48,8 @@
       "-Xep:TryFailThrowable:ERROR",
       "-Xep:UnnecessaryParentheses:ERROR",
       "-Xep:UseCorrectAssertInTests:ERROR",
+      "-XepDisableWarningsInGeneratedCode",  // Disable warnings in gRPC generated code.
+      "-XepExcludedPaths:.*/srcjars/.*"
     ],
   },
 }
@@ -77,6 +79,24 @@
     ],
 }
 
+java_genrule_host {
+    name: "lab-resource-grpc-gen",
+    srcs: [
+        "proto/monitoring/server/lab_resource.proto",
+    ],
+    tools: [
+        "aprotoc",
+        "protoc-gen-grpc-java-plugin",
+        "soong_zip",
+     ],
+     arch: "common",
+     cmd: "$(location aprotoc) -Iexternal/protobuf/src" +
+        " -Itools/tradefederation/core/proto/monitoring/server" +
+        " --plugin=protoc-gen-grpc=$(location protoc-gen-grpc-java-plugin) $(in)" +
+        " --grpc_out=$(genDir) && $(location soong_zip) -o $(out) -C $(genDir) -D $(genDir)",
+     out: ["tradefed-grpc.srcjar"],
+}
+
 python_library_host {
     name: "tradefed-protos-py",
     pkg_path: "atest",
@@ -133,6 +153,7 @@
     srcs: [
         "src/**/*.java",
         "global_configuration/**/*.java",
+        ":lab-resource-grpc-gen",
     ],
     static_libs: [
         "tradefed-common-util",
@@ -159,6 +180,14 @@
         "tradefed-protos",
         "tradefed-isolation-protos",
         "tradefed-lite",
+        "guava",
+        "guava-testlib",
+        "grpc-java",
+        "grpc-java-testing",
+        "grpc-java-netty-shaded",
+        "javax-annotation-api-prebuilt-host-jar",
+        "opencensus-java-api",
+        "opencensus-java-contrib-grpc-metrics",
     ],
     libs: [
         "loganalysis",
@@ -210,7 +239,6 @@
           "-werror " +
           "-package " +
           "-devsite ",
-    create_stubs: false,
 }
 
 sh_binary_host {
diff --git a/README.md b/README.md
index 201d82b..38d8674 100644
--- a/README.md
+++ b/README.md
@@ -7,11 +7,21 @@
 Other test harnesses like Compatibility Test Suite (CTS) and Vendor Test Suite
 (VTS) use TF as a basis and extend it for their particular needs.
 
-Building TF:
+### Building TF:
+
   * source build/envsetup.sh
   * tapas tradefed-all
   * make -j8
 
+### Getting Code Reviewed
+
+    1. Create your change in Gerrit
+    2. Add the reviewer named "Tradefed Codereview" (email: tradefed-codereview@tradefederation.google.com.iam.gserviceaccount.com)
+    3. Review the code review guidance at go/tf-guidelines and go/tradefed-code-reviews
+    4. GWSQ should add a couple of people from the team to review your code and give feedback.
+
+### More information
+
 More information at:
 https://source.android.com/devices/tech/test_infra/tradefed/
 
diff --git a/atest/Android.bp b/atest/Android.bp
index 53553fd..a403173 100644
--- a/atest/Android.bp
+++ b/atest/Android.bp
@@ -134,7 +134,6 @@
         "atest_proto",
     ],
     test_config: "atest_unittests.xml",
-    test_suites: ["general-tests"],
     defaults: ["atest_py2_default"],
 }
 
diff --git a/atest/TEST_MAPPING b/atest/TEST_MAPPING
index 32e6a6d..09a0ffb 100644
--- a/atest/TEST_MAPPING
+++ b/atest/TEST_MAPPING
@@ -28,11 +28,11 @@
     }
   ],
   "presubmit": [
-    {
-      // Host side ATest unittests.
-      "name": "atest_unittests",
-      "host": true
-    },
+//    {
+//      // Host side ATest unittests.
+//      "name": "atest_unittests",
+//      "host": true
+//    },
     {
       // Host side metrics tests.
       "name": "asuite_metrics_lib_tests",
diff --git a/atest/asuite_metrics.py b/atest/asuite_metrics.py
index 8dcd7dc..88fca0a 100644
--- a/atest/asuite_metrics.py
+++ b/atest/asuite_metrics.py
@@ -36,17 +36,17 @@
                           '.config', 'asuite', '.metadata')
 _ANDROID_BUILD_TOP = 'ANDROID_BUILD_TOP'
 
-DUMMY_UUID = '00000000-0000-4000-8000-000000000000'
+UNUSED_UUID = '00000000-0000-4000-8000-000000000000'
 
 
 #pylint: disable=broad-except
-def log_event(metrics_url, dummy_key_fallback=True, **kwargs):
+def log_event(metrics_url, unused_key_fallback=True, **kwargs):
     """Base log event function for asuite backend.
 
     Args:
         metrics_url: String, URL to report metrics to.
-        dummy_key_fallback: Boolean, If True and unable to get grouping key,
-                            use a dummy key otherwise return out. Sometimes we
+        unused_key_fallback: Boolean, If True and unable to get grouping key,
+                            use a unused key otherwise return out. Sometimes we
                             don't want to return metrics for users we are
                             unable to identify. Default True.
         kwargs: Dict, additional fields we want to return metrics for.
@@ -55,9 +55,9 @@
         try:
             key = str(_get_grouping_key())
         except Exception:
-            if not dummy_key_fallback:
+            if not unused_key_fallback:
                 return
-            key = DUMMY_UUID
+            key = UNUSED_UUID
         data = {'grouping_key': key,
                 'run_id': str(uuid.uuid4())}
         if kwargs:
diff --git a/atest/metrics/metrics_base.py b/atest/metrics/metrics_base.py
index 3d5abe8..44b3819 100644
--- a/atest/metrics/metrics_base.py
+++ b/atest/metrics/metrics_base.py
@@ -92,7 +92,7 @@
         _user_key = str(asuite_metrics._get_grouping_key())
     #pylint: disable=broad-except
     except Exception:
-        _user_key = asuite_metrics.DUMMY_UUID
+        _user_key = asuite_metrics.UNUSED_UUID
     _user_type = get_user_type()
     _log_source = ATEST_LOG_SOURCE[_user_type]
     cc = clearcut_client.Clearcut(_log_source)
diff --git a/atest/test_runner_handler.py b/atest/test_runner_handler.py
index 86f42cb..3c18119 100644
--- a/atest/test_runner_handler.py
+++ b/atest/test_runner_handler.py
@@ -94,11 +94,11 @@
     Returns:
         Set of build targets required by the test runners.
     """
-    dummy_result_dir = ''
+    unused_result_dir = ''
     test_runner_build_req = set()
     for test_runner, _ in group_tests_by_test_runners(test_infos):
         test_runner_build_req |= test_runner(
-            dummy_result_dir,
+            unused_result_dir,
             module_info=module_info).get_test_runner_build_reqs()
     return test_runner_build_req
 
diff --git a/atest/unittest_data/path_testing/PathTesting.java b/atest/unittest_data/path_testing/PathTesting.java
index 468307a..2245c67 100644
--- a/atest/unittest_data/path_testing/PathTesting.java
+++ b/atest/unittest_data/path_testing/PathTesting.java
@@ -16,7 +16,7 @@
 
 package android.jank.cts.ui;
 
-/** Dummy Class file for unit tests. */
+/** UNUSED Class file for unit tests. */
 public class SomeClassForTesting {
-    private static final String SOME_DUMMY_VAR = "For testing purposes";
+    private static final String SOME_UNUSED_VAR = "For testing purposes";
 }
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/suite/CertificationSuiteResultReporter.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/suite/CertificationSuiteResultReporter.java
deleted file mode 100644
index c551496..0000000
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/suite/CertificationSuiteResultReporter.java
+++ /dev/null
@@ -1,597 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.compatibility.common.tradefed.result.suite;
-
-import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
-import com.android.compatibility.common.util.DeviceInfo;
-import com.android.compatibility.common.util.ResultHandler;
-import com.android.compatibility.common.util.ResultUploader;
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.config.IConfiguration;
-import com.android.tradefed.config.IConfigurationReceiver;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.config.OptionClass;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.ILogSaver;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.ITestSummaryListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.LogFile;
-import com.android.tradefed.result.LogFileSaver;
-import com.android.tradefed.result.TestRunResult;
-import com.android.tradefed.result.TestSummary;
-import com.android.tradefed.result.suite.IFormatterGenerator;
-import com.android.tradefed.result.suite.SuiteResultReporter;
-import com.android.tradefed.result.suite.XmlFormattedGeneratorReporter;
-import com.android.tradefed.util.FileUtil;
-import com.android.tradefed.util.StreamUtil;
-import com.android.tradefed.util.ZipUtil;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerException;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.stream.StreamResult;
-import javax.xml.transform.stream.StreamSource;
-
-/**
- * Extension of {@link XmlFormattedGeneratorReporter} and {@link SuiteResultReporter} to handle
- * Compatibility specific format and operations.
- */
-@OptionClass(alias = "result-reporter")
-public class CertificationSuiteResultReporter extends XmlFormattedGeneratorReporter
-        implements IConfigurationReceiver, ITestSummaryListener {
-
-    public static final String LATEST_LINK_NAME = "latest";
-    public static final String SUMMARY_FILE = "invocation_summary.txt";
-    public static final String HTLM_REPORT_NAME = "test_result.html";
-    public static final String REPORT_XSL_FILE_NAME = "compatibility_result.xsl";
-    public static final String FAILURE_REPORT_NAME = "test_result_failures_suite.html";
-    public static final String FAILURE_XSL_FILE_NAME = "compatibility_failures.xsl";
-
-    public static final String BUILD_FINGERPRINT = "build_fingerprint";
-
-    @Option(name = "result-server", description = "Server to publish test results.")
-    private String mResultServer;
-
-    @Option(
-            name = "disable-result-posting",
-            description ="Disable result posting into report server."
-    )
-    private boolean mDisableResultPosting = false;
-
-    @Option(name = "include-test-log-tags", description = "Include test log tags in report.")
-    private boolean mIncludeTestLogTags = false;
-
-    @Option(name = "use-log-saver", description = "Also saves generated result with log saver")
-    private boolean mUseLogSaver = false;
-
-    @Option(name = "compress-logs", description = "Whether logs will be saved with compression")
-    private boolean mCompressLogs = true;
-
-    public static final String INCLUDE_HTML_IN_ZIP = "html-in-zip";
-    @Option(name = INCLUDE_HTML_IN_ZIP,
-            description = "Whether failure summary report is included in the zip fie.")
-    private boolean mIncludeHtml = false;
-
-    private CompatibilityBuildHelper mBuildHelper;
-
-    /** The directory containing the results */
-    private File mResultDir = null;
-    /** The directory containing the logs */
-    private File mLogDir = null;
-
-    private ResultUploader mUploader;
-
-    private LogFileSaver mTestLogSaver;
-    /** Invocation level Log saver to receive when files are logged */
-    private ILogSaver mLogSaver;
-    /** Invocation level configuration */
-    private IConfiguration mConfiguration = null;
-
-    private String mReferenceUrl;
-
-    private Map<String, String> mLoggedFiles;
-
-    private static final String[] RESULT_RESOURCES = {
-        "compatibility_result.css",
-        "compatibility_result.xsl",
-        "logo.png"
-    };
-
-    public CertificationSuiteResultReporter() {
-        super();
-        mLoggedFiles = new LinkedHashMap<>();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public final void invocationStarted(IInvocationContext context) {
-        super.invocationStarted(context);
-
-        if (mBuildHelper == null) {
-            mBuildHelper = new CompatibilityBuildHelper(getPrimaryBuildInfo());
-        }
-        if (mResultDir == null) {
-            initializeResultDirectories();
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void testLog(String name, LogDataType type, InputStreamSource stream) {
-        if (name.endsWith(DeviceInfo.FILE_SUFFIX)) {
-            // Handle device info file case
-            testLogDeviceInfo(name, stream);
-            return;
-        }
-        try {
-            File logFile = null;
-            if (mCompressLogs) {
-                try (InputStream inputStream = stream.createInputStream()) {
-                    logFile = mTestLogSaver.saveAndGZipLogData(name, type, inputStream);
-                }
-            } else {
-                try (InputStream inputStream = stream.createInputStream()) {
-                    logFile = mTestLogSaver.saveLogData(name, type, inputStream);
-                }
-            }
-            CLog.d("Saved logs for %s in %s", name, logFile.getAbsolutePath());
-        } catch (IOException e) {
-            CLog.e("Failed to write log for %s", name);
-            CLog.e(e);
-        }
-    }
-
-    /** Write device-info files to the result, invoked only by the master result reporter */
-    private void testLogDeviceInfo(String name, InputStreamSource stream) {
-        try {
-            File ediDir = new File(mResultDir, DeviceInfo.RESULT_DIR_NAME);
-            ediDir.mkdirs();
-            File ediFile = new File(ediDir, name);
-            if (!ediFile.exists()) {
-                // only write this file to the results if not already present
-                FileUtil.writeToFile(stream.createInputStream(), ediFile);
-            }
-        } catch (IOException e) {
-            CLog.w("Failed to write device info %s to result", name);
-            CLog.e(e);
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream,
-            LogFile logFile) {
-        if (mIncludeTestLogTags) {
-            switch (dataType) {
-                case BUGREPORT:
-                case LOGCAT:
-                case PNG:
-                    mLoggedFiles.put(dataName, logFile.getUrl());
-                    break;
-                default:
-                    // Do nothing
-                    break;
-            }
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void putSummary(List<TestSummary> summaries) {
-        for (TestSummary summary : summaries) {
-            if (mReferenceUrl == null && summary.getSummary().getString() != null) {
-                mReferenceUrl = summary.getSummary().getString();
-            }
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setLogSaver(ILogSaver saver) {
-        mLogSaver = saver;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public void setConfiguration(IConfiguration configuration) {
-        mConfiguration = configuration;
-    }
-
-    /**
-     * Create directory structure where results and logs will be written.
-     */
-    private void initializeResultDirectories() {
-        CLog.d("Initializing result directory");
-        // TODO: Clean up start time handling to avoid relying on buildinfo
-        getPrimaryBuildInfo().addBuildAttribute(CompatibilityBuildHelper.START_TIME_MS,
-                Long.toString(getStartTime()));
-        try {
-            mResultDir = mBuildHelper.getResultDir();
-            if (mResultDir != null) {
-                mResultDir.mkdirs();
-            }
-        } catch (FileNotFoundException e) {
-            throw new RuntimeException(e);
-        }
-
-        if (mResultDir == null) {
-            throw new RuntimeException("Result Directory was not created");
-        }
-        if (!mResultDir.exists()) {
-            throw new RuntimeException("Result Directory was not created: " +
-                    mResultDir.getAbsolutePath());
-        }
-
-        CLog.d("Results Directory: %s", mResultDir.getAbsolutePath());
-
-        mUploader = new ResultUploader(mResultServer, mBuildHelper.getSuiteName());
-        try {
-            mLogDir = new File(mBuildHelper.getLogsDir(),
-                    CompatibilityBuildHelper.getDirSuffix(getStartTime()));
-        } catch (FileNotFoundException e) {
-            CLog.e(e);
-        }
-        if (mLogDir != null && mLogDir.mkdirs()) {
-            CLog.d("Created log dir %s", mLogDir.getAbsolutePath());
-        }
-        if (mLogDir == null || !mLogDir.exists()) {
-            throw new IllegalArgumentException(String.format("Could not create log dir %s",
-                    mLogDir.getAbsolutePath()));
-        }
-        if (mTestLogSaver == null) {
-            mTestLogSaver = new LogFileSaver(mLogDir);
-        }
-    }
-
-    @Override
-    public IFormatterGenerator createFormatter() {
-        return new CertificationResultXml(mBuildHelper.getSuiteName(),
-                mBuildHelper.getSuiteVersion(),
-                mBuildHelper.getSuitePlan(),
-                mBuildHelper.getSuiteBuild(),
-                mReferenceUrl,
-                getLogUrl());
-    }
-
-    @Override
-    public void preFormattingSetup(IFormatterGenerator formater) {
-        super.preFormattingSetup(formater);
-        // Log the summary
-        TestSummary summary = getSummary();
-        try {
-            File summaryFile = new File(mResultDir, SUMMARY_FILE);
-            FileUtil.writeToFile(summary.getSummary().toString(), summaryFile);
-        } catch (IOException e) {
-            CLog.e("Failed to save the summary.");
-            CLog.e(e);
-        }
-
-        copyDynamicConfigFiles();
-        copyFormattingFiles(mResultDir, mBuildHelper.getSuiteName());
-    }
-
-    @Override
-    public File createResultDir() throws IOException {
-        return mResultDir;
-    }
-
-    @Override
-    public void postFormattingStep(File resultDir, File reportFile) {
-        super.postFormattingStep(resultDir,reportFile);
-
-        createChecksum(
-                resultDir,
-                getMergedTestRunResults(),
-                getPrimaryBuildInfo().getBuildAttributes().get(BUILD_FINGERPRINT));
-
-        File report = createReport(reportFile);
-        if (report != null) {
-            CLog.i("Viewable report: %s", report.getAbsolutePath());
-        }
-        File failureReport = null;
-        if (mIncludeHtml) {
-            // Create the html report before the zip file.
-            failureReport = createFailureReport(reportFile);
-        }
-        File zippedResults = zipResults(mResultDir);
-        if (!mIncludeHtml) {
-            // Create failure report after zip file so extra data is not uploaded
-            failureReport = createFailureReport(reportFile);
-        }
-        try {
-            if (failureReport.exists()) {
-                CLog.i("Test Result: %s", failureReport.getCanonicalPath());
-            } else {
-                CLog.i("Test Result: %s", reportFile.getCanonicalPath());
-            }
-            Path latestLink = createLatestLinkDirectory(mResultDir.toPath());
-            if (latestLink != null) {
-                CLog.i("Latest results link: " + latestLink.toAbsolutePath());
-            }
-
-            latestLink = createLatestLinkDirectory(mLogDir.toPath());
-            if (latestLink != null) {
-                CLog.i("Latest logs link: " + latestLink.toAbsolutePath());
-            }
-
-            saveLog(reportFile, zippedResults);
-        } catch (IOException e) {
-            CLog.e("Error when handling the post processing of results file:");
-            CLog.e(e);
-        }
-
-        uploadResult(reportFile);
-    }
-
-    /**
-     * Return the path in which log saver persists log files or null if
-     * logSaver is not enabled.
-     */
-    private String getLogUrl() {
-        if (!mUseLogSaver || mLogSaver == null) {
-            return null;
-        }
-
-        return mLogSaver.getLogReportDir().getUrl();
-    }
-
-    /**
-     * Update the "latest" symlink to the newest result directory. CTS specific.
-     */
-    private Path createLatestLinkDirectory(Path directory) {
-        Path link = null;
-
-        Path parent = directory.getParent();
-
-        if (parent != null) {
-            link = parent.resolve(LATEST_LINK_NAME);
-            try {
-                // if latest already exists, we have to remove it before creating
-                Files.deleteIfExists(link);
-                Files.createSymbolicLink(link, directory);
-            } catch (IOException ioe) {
-                CLog.e("Exception while attempting to create 'latest' link to: [%s]",
-                    directory);
-                CLog.e(ioe);
-                return null;
-            } catch (UnsupportedOperationException uoe) {
-                CLog.e("Failed to create 'latest' symbolic link - unsupported operation");
-                return null;
-            }
-        }
-        return link;
-    }
-
-    /**
-     * move the dynamic config files to the results directory
-     */
-    private void copyDynamicConfigFiles() {
-        File configDir = new File(mResultDir, "config");
-        if (!configDir.mkdir()) {
-            CLog.w("Failed to make dynamic config directory \"%s\" in the result",
-                    configDir.getAbsolutePath());
-        }
-
-        Set<String> uniqueModules = new HashSet<>();
-        // Check each build of the invocation, in case of multi-device invocation.
-        for (IBuildInfo buildInfo : getInvocationContext().getBuildInfos()) {
-            CompatibilityBuildHelper helper = new CompatibilityBuildHelper(buildInfo);
-            Map<String, File> dcFiles = helper.getDynamicConfigFiles();
-            for (String moduleName : dcFiles.keySet()) {
-                File srcFile = dcFiles.get(moduleName);
-                if (!uniqueModules.contains(moduleName)) {
-                    // have not seen config for this module yet, copy into result
-                    File destFile = new File(configDir, moduleName + ".dynamic");
-                    try {
-                        FileUtil.copyFile(srcFile, destFile);
-                        uniqueModules.add(moduleName); // Add to uniqueModules if copy succeeds
-                    } catch (IOException e) {
-                        CLog.w("Failure when copying config file \"%s\" to \"%s\" for module %s",
-                                srcFile.getAbsolutePath(), destFile.getAbsolutePath(), moduleName);
-                        CLog.e(e);
-                    }
-                }
-                FileUtil.deleteFile(srcFile);
-            }
-        }
-    }
-
-    /**
-     * Copy the xml formatting files stored in this jar to the results directory. CTS specific.
-     *
-     * @param resultsDir
-     */
-    private void copyFormattingFiles(File resultsDir, String suiteName) {
-        for (String resultFileName : RESULT_RESOURCES) {
-            InputStream configStream = CertificationResultXml.class.getResourceAsStream(
-                    String.format("/report/%s-%s", suiteName, resultFileName));
-            if (configStream == null) {
-                // If suite specific files are not available, fallback to common.
-                configStream = CertificationResultXml.class.getResourceAsStream(
-                    String.format("/report/%s", resultFileName));
-            }
-            if (configStream != null) {
-                File resultFile = new File(resultsDir, resultFileName);
-                try {
-                    FileUtil.writeToFile(configStream, resultFile);
-                } catch (IOException e) {
-                    CLog.w("Failed to write %s to file", resultFileName);
-                }
-            } else {
-                CLog.w("Failed to load %s from jar", resultFileName);
-            }
-        }
-    }
-
-    /**
-     * When enabled, save log data using log saver
-     */
-    private void saveLog(File resultFile, File zippedResults) throws IOException {
-        if (!mUseLogSaver) {
-            return;
-        }
-
-        FileInputStream fis = null;
-        LogFile logFile = null;
-        try {
-            fis = new FileInputStream(resultFile);
-            logFile = mLogSaver.saveLogData("log-result", LogDataType.XML, fis);
-            CLog.d("Result XML URL: %s", logFile.getUrl());
-            logReportFiles(mConfiguration, resultFile, resultFile.getName(), LogDataType.XML);
-        } catch (IOException ioe) {
-            CLog.e("error saving XML with log saver");
-            CLog.e(ioe);
-        } finally {
-            StreamUtil.close(fis);
-        }
-        // Save the full results folder.
-        if (zippedResults != null) {
-            FileInputStream zipResultStream = null;
-            try {
-                zipResultStream = new FileInputStream(zippedResults);
-                logFile = mLogSaver.saveLogData("results", LogDataType.ZIP, zipResultStream);
-                CLog.d("Result zip URL: %s", logFile.getUrl());
-                logReportFiles(mConfiguration, zippedResults, "results", LogDataType.ZIP);
-            } finally {
-                StreamUtil.close(zipResultStream);
-            }
-        }
-    }
-
-    /**
-     * Zip the contents of the given results directory. CTS specific.
-     *
-     * @param resultsDir
-     */
-    private static File zipResults(File resultsDir) {
-        File zipResultFile = null;
-        try {
-            // create a file in parent directory, with same name as resultsDir
-            zipResultFile = new File(resultsDir.getParent(), String.format("%s.zip",
-                    resultsDir.getName()));
-            ZipUtil.createZip(resultsDir, zipResultFile);
-        } catch (IOException e) {
-            CLog.w("Failed to create zip for %s", resultsDir.getName());
-        }
-        return zipResultFile;
-    }
-
-    /**
-     * When enabled, upload the result to a server. CTS specific.
-     */
-    private void uploadResult(File resultFile) {
-        if (mResultServer != null && !mResultServer.trim().isEmpty() && !mDisableResultPosting) {
-            try {
-                CLog.d("Result Server: %d", mUploader.uploadResult(resultFile, mReferenceUrl));
-            } catch (IOException ioe) {
-                CLog.e("IOException while uploading result.");
-                CLog.e(ioe);
-            }
-        }
-    }
-
-    /** Generate html report. */
-    private File createReport(File inputXml) {
-        File report = new File(inputXml.getParentFile(), HTLM_REPORT_NAME);
-        try (InputStream xslStream =
-                        new FileInputStream(
-                                new File(inputXml.getParentFile(), REPORT_XSL_FILE_NAME));
-                OutputStream outputStream = new FileOutputStream(report)) {
-            Transformer transformer =
-                    TransformerFactory.newInstance().newTransformer(new StreamSource(xslStream));
-            transformer.transform(new StreamSource(inputXml), new StreamResult(outputStream));
-        } catch (IOException | TransformerException ignored) {
-            CLog.e(ignored);
-            FileUtil.deleteFile(report);
-            return null;
-        }
-        return report;
-    }
-
-    /**
-     * Generate html report listing an failed tests. CTS specific.
-     */
-    private File createFailureReport(File inputXml) {
-        File failureReport = new File(inputXml.getParentFile(), FAILURE_REPORT_NAME);
-        try (InputStream xslStream = ResultHandler.class.getResourceAsStream(
-                String.format("/report/%s", FAILURE_XSL_FILE_NAME));
-             OutputStream outputStream = new FileOutputStream(failureReport)) {
-
-            Transformer transformer = TransformerFactory.newInstance().newTransformer(
-                    new StreamSource(xslStream));
-            transformer.transform(new StreamSource(inputXml), new StreamResult(outputStream));
-        } catch (IOException | TransformerException ignored) {
-            CLog.e(ignored);
-        }
-        return failureReport;
-    }
-
-    /**
-     * Generates a checksum files based on the results.
-     */
-    private void createChecksum(File resultDir, Collection<TestRunResult> results,
-            String buildFingerprint) {
-        CertificationChecksumHelper.tryCreateChecksum(resultDir, results, buildFingerprint);
-    }
-
-    /** Re-log a result file to all reporters so they are aware of it. */
-    private void logReportFiles(
-            IConfiguration configuration, File resultFile, String dataName, LogDataType type) {
-        if (configuration == null) {
-            return;
-        }
-        List<ITestInvocationListener> listeners = configuration.getTestInvocationListeners();
-        try (FileInputStreamSource source = new FileInputStreamSource(resultFile)) {
-            for (ITestInvocationListener listener : listeners) {
-                if (listener.equals(this)) {
-                    // Avoid logging agaisnt itself
-                    continue;
-                }
-                listener.testLog(dataName, type, source);
-            }
-        }
-    }
-}
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/suite/PreviousSessionFileCopier.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/suite/PreviousSessionFileCopier.java
deleted file mode 100644
index 295692b..0000000
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/suite/PreviousSessionFileCopier.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.compatibility.common.tradefed.result.suite;
-
-import com.android.annotations.VisibleForTesting;
-import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
-import com.android.compatibility.common.util.ChecksumReporter;
-import com.android.compatibility.common.util.ResultHandler;
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.util.FileUtil;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Recursively copy all the files from a previous session into the current one if they don't exists
- * already.
- */
-public class PreviousSessionFileCopier implements ITestInvocationListener {
-
-    private static final List<String> NOT_RETRY_FILES =
-            Arrays.asList(
-                    ChecksumReporter.NAME,
-                    ChecksumReporter.PREV_NAME,
-                    ResultHandler.FAILURE_REPORT_NAME,
-                    CertificationSuiteResultReporter.HTLM_REPORT_NAME,
-                    CertificationSuiteResultReporter.FAILURE_REPORT_NAME,
-                    CertificationSuiteResultReporter.SUMMARY_FILE,
-                    CertificationChecksumHelper.NAME,
-                    "diffs",
-                    "proto");
-
-    private CompatibilityBuildHelper mBuildHelper;
-    private File mPreviousSessionDir = null;
-
-    /** Sets the previous session directory to copy from. */
-    public void setPreviousSessionDir(File previousSessionDir) {
-        mPreviousSessionDir = previousSessionDir;
-    }
-
-    @Override
-    public void invocationStarted(IInvocationContext context) {
-        if (mBuildHelper == null) {
-            mBuildHelper = createCompatibilityHelper(context.getBuildInfos().get(0));
-        }
-    }
-
-    @Override
-    public void invocationEnded(long elapsedTime) {
-        if (mPreviousSessionDir == null) {
-            CLog.e("Could not copy previous sesson files.");
-            return;
-        }
-        File resultDir = getResultDirectory();
-        copyRetryFiles(mPreviousSessionDir, resultDir);
-    }
-
-    @VisibleForTesting
-    protected CompatibilityBuildHelper createCompatibilityHelper(IBuildInfo info) {
-        return new CompatibilityBuildHelper(info);
-    }
-
-    /**
-     * Recursively copy any other files found in the previous session's result directory to the new
-     * result directory, so long as they don't already exist. For example, a "screenshots" directory
-     * generated in a previous session by a passing test will not be generated on retry unless
-     * copied from the old result directory.
-     *
-     * @param oldDir
-     * @param newDir
-     */
-    private void copyRetryFiles(File oldDir, File newDir) {
-        File[] oldChildren = oldDir.listFiles();
-        for (File oldChild : oldChildren) {
-            if (NOT_RETRY_FILES.contains(oldChild.getName())) {
-                continue; // do not copy this file/directory or its children
-            }
-            File newChild = new File(newDir, oldChild.getName());
-            if (!newChild.exists()) {
-                // If this old file or directory doesn't exist in new dir, simply copy it
-                try {
-                    CLog.d("Copying %s to new session.", oldChild.getName());
-                    if (oldChild.isDirectory()) {
-                        FileUtil.recursiveCopy(oldChild, newChild);
-                    } else {
-                        FileUtil.copyFile(oldChild, newChild);
-                    }
-                } catch (IOException e) {
-                    CLog.w("Failed to copy file \"%s\" from previous session", oldChild.getName());
-                }
-            } else if (oldChild.isDirectory() && newChild.isDirectory()) {
-                // If both children exist as directories, make sure the children of the old child
-                // directory exist in the new child directory.
-                copyRetryFiles(oldChild, newChild);
-            }
-        }
-    }
-
-    private File getResultDirectory() {
-        File resultDir = null;
-        try {
-            resultDir = mBuildHelper.getResultDir();
-            if (resultDir != null) {
-                resultDir.mkdirs();
-            }
-        } catch (FileNotFoundException e) {
-            throw new RuntimeException(e);
-        }
-        if (resultDir == null) {
-            throw new RuntimeException("Result Directory was not created");
-        }
-        if (!resultDir.exists()) {
-            throw new RuntimeException(
-                    "Result Directory was not created: " + resultDir.getAbsolutePath());
-        }
-        CLog.d("Results Directory: %s", resultDir.getAbsolutePath());
-        return resultDir;
-    }
-}
diff --git a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/ResultReporterTest.java b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/ResultReporterTest.java
deleted file mode 100644
index 805a2ad..0000000
--- a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/ResultReporterTest.java
+++ /dev/null
@@ -1,622 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.compatibility.common.tradefed.result;
-
-import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
-import com.android.compatibility.common.tradefed.build.CompatibilityBuildProvider;
-import com.android.compatibility.common.util.DeviceInfo;
-import com.android.compatibility.common.util.ICaseResult;
-import com.android.compatibility.common.util.IInvocationResult;
-import com.android.compatibility.common.util.IModuleResult;
-import com.android.compatibility.common.util.ITestResult;
-import com.android.compatibility.common.util.TestStatus;
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.invoker.InvocationContext;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.ByteArrayInputStreamSource;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.TestDescription;
-import com.android.tradefed.util.AbiUtils;
-import com.android.tradefed.util.FileUtil;
-
-import junit.framework.TestCase;
-
-import java.io.File;
-import java.io.FileFilter;
-import java.util.HashMap;
-import java.util.List;
-
-/**
- * Unit tests for {@link ResultReporter}
- */
-public class ResultReporterTest extends TestCase {
-
-    private static final String ROOT_PROPERTY = "TESTS_ROOT";
-    private static final String SUITE_NAME = "TESTS";
-    private static final String BUILD_NUMBER = "2";
-    private static final String SUITE_PLAN = "cts";
-    private static final String DYNAMIC_CONFIG_URL = "";
-    private static final String ROOT_DIR_NAME = "root";
-    private static final String BASE_DIR_NAME = "android-tests";
-    private static final String TESTCASES = "testcases";
-    private static final String NAME = "ModuleName";
-    private static final String ABI = "mips64";
-    private static final String ID = AbiUtils.createId(ABI, NAME);
-    private static final String CLASS = "android.test.FoorBar";
-    private static final String METHOD_1 = "testBlah1";
-    private static final String METHOD_2 = "testBlah2";
-    private static final String METHOD_3 = "testBlah3";
-    private static final String TEST_1 = String.format("%s#%s", CLASS, METHOD_1);
-    private static final String TEST_2 = String.format("%s#%s", CLASS, METHOD_2);
-    private static final String TEST_3 = String.format("%s#%s", CLASS, METHOD_3);
-    private static final String STACK_TRACE = "Something small is not alright\n " +
-            "at four.big.insects.Marley.sing(Marley.java:10)";
-    private static final String RESULT_DIR = "result123";
-    private static final String[] FORMATTING_FILES = {
-        "compatibility_result.css",
-        "compatibility_result.xsl",
-        "logo.png"};
-
-    private ResultReporter mReporter;
-    private IBuildInfo mBuildInfo;
-    private IInvocationContext mContext;
-    private CompatibilityBuildHelper mBuildHelper;
-
-    private File mRoot = null;
-    private File mBase = null;
-    private File mTests = null;
-
-    @Override
-    public void setUp() throws Exception {
-        mReporter = new ResultReporter();
-        mRoot = FileUtil.createTempDir(ROOT_DIR_NAME);
-        mBase = new File(mRoot, BASE_DIR_NAME);
-        mBase.mkdirs();
-        mTests = new File(mBase, TESTCASES);
-        mTests.mkdirs();
-        System.setProperty(ROOT_PROPERTY, mRoot.getAbsolutePath());
-        CompatibilityBuildProvider provider = new CompatibilityBuildProvider() {
-            @Override
-            protected String getSuiteInfoName() {
-                return SUITE_NAME;
-            }
-            @Override
-            protected String getSuiteInfoBuildNumber() {
-                return BUILD_NUMBER;
-            }
-            @Override
-            protected String getSuiteInfoVersion() {
-                return BUILD_NUMBER;
-            }
-        };
-        OptionSetter setter = new OptionSetter(provider);
-        setter.setOptionValue("plan", SUITE_PLAN);
-        setter.setOptionValue("dynamic-config-url", DYNAMIC_CONFIG_URL);
-        mBuildInfo = provider.getBuild();
-        mBuildHelper = new CompatibilityBuildHelper(mBuildInfo);
-        mContext = new InvocationContext();
-        mContext.addDeviceBuildInfo("fakeDevice", mBuildInfo);
-    }
-
-    @Override
-    public void tearDown() throws Exception {
-        mReporter = null;
-        FileUtil.recursiveDelete(mRoot);
-    }
-
-    public void testSetup() throws Exception {
-        mReporter.invocationStarted(mContext);
-        // Should have created a directory for the logs
-        File[] children = mBuildHelper.getLogsDir().listFiles();
-        assertTrue("Didn't create logs dir", children.length == 1 && children[0].isDirectory());
-        // Should have created a directory for the results
-        children = mBuildHelper.getResultsDir().listFiles();
-        assertTrue("Didn't create results dir", children.length == 1 && children[0].isDirectory());
-        mReporter.invocationEnded(10);
-        // Should have created a zip file
-        children = mBuildHelper.getResultsDir().listFiles(new FileFilter() {
-            @Override
-            public boolean accept(File pathname) {
-                return pathname.getName().endsWith(".zip");
-            }
-        });
-        assertTrue("Didn't create results zip",
-                children.length == 1 && children[0].isFile() && children[0].length() > 0);
-    }
-
-    public void testResultReporting() throws Exception {
-        mReporter.invocationStarted(mContext);
-        mReporter.testRunStarted(ID, 2);
-        TestDescription test1 = new TestDescription(CLASS, METHOD_1);
-        mReporter.testStarted(test1);
-        mReporter.testEnded(test1, new HashMap<String, Metric>());
-        TestDescription test2 = new TestDescription(CLASS, METHOD_2);
-        mReporter.testStarted(test2);
-        mReporter.testFailed(test2, STACK_TRACE);
-        TestDescription test3 = new TestDescription(CLASS, METHOD_3);
-        mReporter.testStarted(test3);
-        mReporter.testFailed(test3, STACK_TRACE);
-        mReporter.testEnded(test3, new HashMap<String, Metric>());
-        mReporter.testRunEnded(10, new HashMap<String, Metric>());
-        mReporter.invocationEnded(10);
-        IInvocationResult result = mReporter.getResult();
-        assertEquals("Expected 1 pass", 1, result.countResults(TestStatus.PASS));
-        assertEquals("Expected 2 failures", 2, result.countResults(TestStatus.FAIL));
-        List<IModuleResult> modules = result.getModules();
-        assertEquals("Expected 1 module", 1, modules.size());
-        IModuleResult module = modules.get(0);
-        assertTrue(module.isDone());
-        assertEquals("Incorrect ID", ID, module.getId());
-        List<ICaseResult> caseResults = module.getResults();
-        assertEquals("Expected 1 test case", 1, caseResults.size());
-        ICaseResult caseResult = caseResults.get(0);
-        List<ITestResult> testResults = caseResult.getResults();
-        assertEquals("Expected 3 tests", 3, testResults.size());
-        ITestResult result1 = caseResult.getResult(METHOD_1);
-        assertNotNull(String.format("Expected result for %s", TEST_1), result1);
-        assertEquals(String.format("Expected pass for %s", TEST_1), TestStatus.PASS,
-                result1.getResultStatus());
-        ITestResult result2 = caseResult.getResult(METHOD_2);
-        assertNotNull(String.format("Expected result for %s", TEST_2), result2);
-        assertEquals(String.format("Expected fail for %s", TEST_2), TestStatus.FAIL,
-                result2.getResultStatus());
-        ITestResult result3 = caseResult.getResult(METHOD_3);
-        assertNotNull(String.format("Expected result for %s", TEST_3), result3);
-        assertEquals(String.format("Expected fail for %s", TEST_3), TestStatus.FAIL,
-                result3.getResultStatus());
-    }
-
-    private void makeTestRun(String[] methods, boolean[] passes) {
-        mReporter.testRunStarted(ID, methods.length);
-
-        for (int i = 0; i < methods.length; i++) {
-            TestDescription test = new TestDescription(CLASS, methods[i]);
-            mReporter.testStarted(test);
-            if (!passes[i]) {
-                mReporter.testFailed(test, STACK_TRACE);
-            }
-            mReporter.testEnded(test, new HashMap<String, Metric>());
-        }
-
-        mReporter.testRunEnded(10, new HashMap<String, Metric>());
-    }
-
-    public void testRepeatedExecutions() throws Exception {
-        String[] methods = new String[] {METHOD_1, METHOD_2, METHOD_3};
-
-        mReporter.invocationStarted(mContext);
-
-        makeTestRun(methods, new boolean[] {true, false, true});
-        makeTestRun(methods, new boolean[] {true, false, false});
-        makeTestRun(methods, new boolean[] {true, true, true});
-
-        mReporter.invocationEnded(10);
-
-        // Verification
-
-        IInvocationResult result = mReporter.getResult();
-        assertEquals("Expected 1 pass", 1, result.countResults(TestStatus.PASS));
-        assertEquals("Expected 2 failures", 2, result.countResults(TestStatus.FAIL));
-        List<IModuleResult> modules = result.getModules();
-        assertEquals("Expected 1 module", 1, modules.size());
-        IModuleResult module = modules.get(0);
-        assertEquals("Incorrect ID", ID, module.getId());
-        List<ICaseResult> caseResults = module.getResults();
-        assertEquals("Expected 1 test case", 1, caseResults.size());
-        ICaseResult caseResult = caseResults.get(0);
-        List<ITestResult> testResults = caseResult.getResults();
-        assertEquals("Expected 3 tests", 3, testResults.size());
-
-        // Test 1 details
-        ITestResult result1 = caseResult.getResult(METHOD_1);
-        assertNotNull(String.format("Expected result for %s", TEST_1), result1);
-        assertEquals(String.format("Expected pass for %s", TEST_1), TestStatus.PASS,
-                result1.getResultStatus());
-
-        // Test 2 details
-        ITestResult result2 = caseResult.getResult(METHOD_2);
-        assertNotNull(String.format("Expected result for %s", TEST_2), result2);
-        assertEquals(String.format("Expected fail for %s", TEST_2), TestStatus.FAIL,
-                result2.getResultStatus());
-        // TODO: Define requirement. Should this result have multiple stack traces?
-        assertEquals(result2.getStackTrace(), STACK_TRACE);
-
-        // Test 3 details
-        ITestResult result3 = caseResult.getResult(METHOD_3);
-        assertNotNull(String.format("Expected result for %s", TEST_3), result3);
-        assertEquals(String.format("Expected fail for %s", TEST_3), TestStatus.FAIL,
-                result3.getResultStatus());
-        assertEquals(result3.getStackTrace(), STACK_TRACE);
-    }
-
-    public void testRetry() throws Exception {
-        mReporter.invocationStarted(mContext);
-
-        // Set up IInvocationResult with existing results from previous session
-        mReporter.testRunStarted(ID, 2);
-        IInvocationResult invocationResult = mReporter.getResult();
-        IModuleResult moduleResult = invocationResult.getOrCreateModule(ID);
-        ICaseResult caseResult = moduleResult.getOrCreateResult(CLASS);
-        ITestResult testResult1 = caseResult.getOrCreateResult(METHOD_1);
-        testResult1.setResultStatus(TestStatus.PASS);
-        testResult1.setRetry(true);
-        ITestResult testResult2 = caseResult.getOrCreateResult(METHOD_2);
-        testResult2.setResultStatus(TestStatus.FAIL);
-        testResult2.setStackTrace(STACK_TRACE);
-        testResult2.setRetry(true);
-
-        // Flip results for the current session
-        TestDescription test1 = new TestDescription(CLASS, METHOD_1);
-        mReporter.testStarted(test1);
-        mReporter.testFailed(test1, STACK_TRACE);
-        mReporter.testEnded(test1, new HashMap<String, Metric>());
-        TestDescription test2 = new TestDescription(CLASS, METHOD_2);
-        mReporter.testStarted(test2);
-        mReporter.testEnded(test2, new HashMap<String, Metric>());
-
-        mReporter.testRunEnded(10, new HashMap<String, Metric>());
-        mReporter.invocationEnded(10);
-
-        // Verification that results have been overwritten.
-        IInvocationResult result = mReporter.getResult();
-        assertEquals("Expected 1 pass", 1, result.countResults(TestStatus.PASS));
-        assertEquals("Expected 1 failure", 1, result.countResults(TestStatus.FAIL));
-        List<IModuleResult> modules = result.getModules();
-        assertEquals("Expected 1 module", 1, modules.size());
-        IModuleResult module = modules.get(0);
-        List<ICaseResult> cases = module.getResults();
-        assertEquals("Expected 1 test case", 1, cases.size());
-        ICaseResult case1 = cases.get(0);
-        List<ITestResult> testResults = case1.getResults();
-        assertEquals("Expected 2 tests", 2, testResults.size());
-
-        // Test 1 details
-        ITestResult finalTestResult1 = case1.getResult(METHOD_1);
-        assertNotNull(String.format("Expected result for %s", TEST_1), finalTestResult1);
-        assertEquals(String.format("Expected fail for %s", TEST_1), TestStatus.FAIL,
-                finalTestResult1.getResultStatus());
-        assertEquals(finalTestResult1.getStackTrace(), STACK_TRACE);
-
-        // Test 2 details
-        ITestResult finalTestResult2 = case1.getResult(METHOD_2);
-        assertNotNull(String.format("Expected result for %s", TEST_2), finalTestResult2);
-        assertEquals(String.format("Expected pass for %s", TEST_2), TestStatus.PASS,
-                finalTestResult2.getResultStatus());
-    }
-
-    public void testRetryCanSetDone() throws Exception {
-        mReporter.invocationStarted(mContext);
-        // Set mCanMarkDone directly (otherwise we must build result directory, write XML, and
-        // perform actual retry)
-        mReporter.mCanMarkDone = true;
-        // Set up IInvocationResult with existing results from previous session
-        IInvocationResult invocationResult = mReporter.getResult();
-        IModuleResult moduleResult = invocationResult.getOrCreateModule(ID);
-        moduleResult.initializeDone(false);
-        ICaseResult caseResult = moduleResult.getOrCreateResult(CLASS);
-        ITestResult testResult1 = caseResult.getOrCreateResult(METHOD_1);
-        testResult1.setResultStatus(TestStatus.PASS);
-        testResult1.setRetry(true);
-        ITestResult testResult2 = caseResult.getOrCreateResult(METHOD_2);
-        testResult2.setResultStatus(TestStatus.FAIL);
-        testResult2.setStackTrace(STACK_TRACE);
-        testResult2.setRetry(true);
-
-        // Assume no additional filtering is applied to retry, and all tests for the module have
-        // been collected. Thus, module "done" value should switch.
-        mReporter.testRunStarted(ID, 1);
-
-        TestDescription test2 = new TestDescription(CLASS, METHOD_2);
-        mReporter.testStarted(test2);
-        mReporter.testEnded(test2, new HashMap<String, Metric>());
-
-        mReporter.testRunEnded(10, new HashMap<String, Metric>());
-        mReporter.invocationEnded(10);
-
-        // Verification that results have been overwritten.
-        IInvocationResult result = mReporter.getResult();
-        assertEquals("Expected 2 pass", 2, result.countResults(TestStatus.PASS));
-        assertEquals("Expected 0 failures", 0, result.countResults(TestStatus.FAIL));
-        List<IModuleResult> modules = result.getModules();
-        assertEquals("Expected 1 module", 1, modules.size());
-        IModuleResult module = modules.get(0);
-        assertTrue("Module should be marked done", module.isDone());
-    }
-
-    public void testRetryCannotSetDone() throws Exception {
-        mReporter.invocationStarted(mContext);
-        // Set mCanMarkDone directly (otherwise we must build result directory, write XML, and
-        // perform actual retry)
-        mReporter.mCanMarkDone = false;
-        // Set up IInvocationResult with existing results from previous session
-        IInvocationResult invocationResult = mReporter.getResult();
-        IModuleResult moduleResult = invocationResult.getOrCreateModule(ID);
-        moduleResult.setDone(false);
-        ICaseResult caseResult = moduleResult.getOrCreateResult(CLASS);
-        ITestResult testResult1 = caseResult.getOrCreateResult(METHOD_1);
-        testResult1.setResultStatus(TestStatus.PASS);
-        testResult1.setRetry(true);
-        ITestResult testResult2 = caseResult.getOrCreateResult(METHOD_2);
-        testResult2.setResultStatus(TestStatus.FAIL);
-        testResult2.setStackTrace(STACK_TRACE);
-        testResult2.setRetry(true);
-
-        // Since using retry-type failed option, we only run previously failed test
-        // and don't run any non-executed tests, so module "done" value should not switch.
-        mReporter.testRunStarted(ID, 1);
-
-        TestDescription test2 = new TestDescription(CLASS, METHOD_2);
-        mReporter.testStarted(test2);
-        mReporter.testEnded(test2, new HashMap<String, Metric>());
-
-        mReporter.testRunEnded(10, new HashMap<String, Metric>());
-        mReporter.invocationEnded(10);
-
-        // Verification that results have been overwritten.
-        IInvocationResult result = mReporter.getResult();
-        assertEquals("Expected 2 pass", 2, result.countResults(TestStatus.PASS));
-        assertEquals("Expected 0 failures", 0, result.countResults(TestStatus.FAIL));
-        List<IModuleResult> modules = result.getModules();
-        assertEquals("Expected 1 module", 1, modules.size());
-        IModuleResult module = modules.get(0);
-        assertFalse("Module should not be marked done", module.isDone());
-    }
-
-    public void testResultReporting_moduleNotDone() throws Exception {
-        mReporter.invocationStarted(mContext);
-        mReporter.testRunStarted(ID, 2);
-        TestDescription test1 = new TestDescription(CLASS, METHOD_1);
-        mReporter.testStarted(test1);
-        mReporter.testEnded(test1, new HashMap<String, Metric>());
-        mReporter.testRunFailed("error");
-        mReporter.testRunEnded(10, new HashMap<String, Metric>());
-        mReporter.invocationEnded(10);
-        IInvocationResult result = mReporter.getResult();
-        assertEquals("Expected 1 pass", 1, result.countResults(TestStatus.PASS));
-        assertEquals("Expected 0 failures", 0, result.countResults(TestStatus.FAIL));
-        List<IModuleResult> modules = result.getModules();
-        assertEquals("Expected 1 module", 1, modules.size());
-        IModuleResult module = modules.get(0);
-
-        // Ensure module is reported as not done
-        assertFalse(module.isDone());
-        assertEquals("Incorrect ID", ID, module.getId());
-        List<ICaseResult> caseResults = module.getResults();
-        assertEquals("Expected 1 test case", 1, caseResults.size());
-        ICaseResult caseResult = caseResults.get(0);
-        List<ITestResult> testResults = caseResult.getResults();
-        assertEquals("Expected 1 tests", 1, testResults.size());
-        ITestResult result1 = caseResult.getResult(METHOD_1);
-        assertNotNull(String.format("Expected result for %s", TEST_1), result1);
-        assertEquals(String.format("Expected pass for %s", TEST_1), TestStatus.PASS,
-                result1.getResultStatus());
-    }
-
-    public void testResultReporting_moduleNotDone_noTests() throws Exception {
-        mReporter.invocationStarted(mContext);
-        mReporter.testRunStarted(ID, 0);
-        mReporter.testRunFailed("error"); // test run failure should prevent marking module "done"
-        mReporter.testRunEnded(10, new HashMap<String, String>());
-        mReporter.invocationEnded(10);
-        IInvocationResult result = mReporter.getResult();
-        assertEquals("Expected 0 pass", 0, result.countResults(TestStatus.PASS));
-        assertEquals("Expected 0 failures", 0, result.countResults(TestStatus.FAIL));
-        List<IModuleResult> modules = result.getModules();
-        assertEquals("Expected 1 module", 1, modules.size());
-        IModuleResult module = modules.get(0);
-        assertEquals("Incorrect ID", ID, module.getId());
-        // Ensure module is reported as not done
-        assertFalse(module.isDone());
-    }
-
-    public void testResultReporting_moduleDone_noTests() throws Exception {
-        mReporter.invocationStarted(mContext);
-        mReporter.testRunStarted(ID, 0);
-        // Lack of test run failure should allow module to be marked "done"
-        mReporter.testRunEnded(10, new HashMap<String, String>());
-        mReporter.invocationEnded(10);
-        IInvocationResult result = mReporter.getResult();
-        assertEquals("Expected 0 pass", 0, result.countResults(TestStatus.PASS));
-        assertEquals("Expected 0 failures", 0, result.countResults(TestStatus.FAIL));
-        List<IModuleResult> modules = result.getModules();
-        assertEquals("Expected 1 module", 1, modules.size());
-        IModuleResult module = modules.get(0);
-        assertEquals("Incorrect ID", ID, module.getId());
-        // Ensure module is reported as done
-        assertTrue(module.isDone());
-    }
-
-    public void testCopyFormattingFiles() throws Exception {
-        File resultDir = new File(mBuildHelper.getResultsDir(), RESULT_DIR);
-        resultDir.mkdirs();
-        ResultReporter.copyFormattingFiles(resultDir, SUITE_NAME);
-        for (String filename : FORMATTING_FILES) {
-            File file = new File(resultDir, filename);
-            assertTrue(String.format("%s (%s) was not created", filename, file.getAbsolutePath()),
-                    file.exists() && file.isFile() && file.length() > 0);
-        }
-    }
-
-    /**
-     * Ensure that when {@link ResultReporter#testLog(String, LogDataType, InputStreamSource)} is
-     * called, a single invocation result folder is created and populated.
-     */
-    public void testTestLog() throws Exception {
-        InputStreamSource fakeData = new ByteArrayInputStreamSource("test".getBytes());
-        mReporter.invocationStarted(mContext);
-        mReporter.testLog("test1", LogDataType.LOGCAT, fakeData);
-        // date folder
-        assertEquals(1, mBuildHelper.getLogsDir().list().length);
-        // inv_ folder
-        assertEquals(1, mBuildHelper.getLogsDir().listFiles()[0].list().length);
-        // actual logs
-        assertEquals(1, mBuildHelper.getLogsDir().listFiles()[0].listFiles()[0].list().length);
-        mReporter.testLog("test2", LogDataType.LOGCAT, fakeData);
-        // date folder
-        assertEquals(1, mBuildHelper.getLogsDir().list().length);
-        // inv_ folder
-        assertEquals(1, mBuildHelper.getLogsDir().listFiles()[0].list().length);
-        // actual logs
-        assertEquals(2, mBuildHelper.getLogsDir().listFiles()[0].listFiles()[0].list().length);
-    }
-
-    /**
-     * Ensure that when {@link ResultReporter#testLog(String, LogDataType, InputStreamSource)} is
-     * called for host-side device info, a device info file is created in the result
-     */
-    public void testTestLogWithDeviceInfo() throws Exception {
-        InputStreamSource fakeData = new ByteArrayInputStreamSource("test".getBytes());
-        String deviceInfoName = String.format("Test%s", DeviceInfo.FILE_SUFFIX);
-        mReporter.invocationStarted(mContext);
-        mReporter.testLog(deviceInfoName, LogDataType.TEXT, fakeData);
-        File deviceInfoFolder = new File(mBuildHelper.getResultDir(), DeviceInfo.RESULT_DIR_NAME);
-        // assert device info folder was created
-        assertTrue(deviceInfoFolder.exists());
-        File[] deviceInfoFiles = deviceInfoFolder.listFiles();
-        // assert that one file was written to the folder
-        assertEquals(1, deviceInfoFiles.length);
-        File deviceInfoFile = deviceInfoFiles[0];
-        // assert device info file has been named correctly
-        assertEquals(deviceInfoName, deviceInfoFile.getName());
-        // assert contents of the file
-        assertEquals("test", FileUtil.readStringFromFile(deviceInfoFile));
-    }
-
-    /** Ensure that the module is not marked done if any of the shard fails. */
-    public void testResultReporter_sharded() throws Exception {
-        ResultReporter shard1 = new ResultReporter(mReporter);
-        ResultReporter shard2 = new ResultReporter(mReporter);
-
-        mReporter.invocationStarted(mContext);
-        shard1.invocationStarted(mContext);
-        shard2.invocationStarted(mContext);
-
-        // First shard is good
-        shard1.testRunStarted(ID, 1);
-        TestDescription test1 = new TestDescription(CLASS, METHOD_1);
-        shard1.testStarted(test1);
-        shard1.testEnded(test1, new HashMap<String, Metric>());
-        shard1.testRunEnded(10, new HashMap<String, Metric>());
-        shard1.invocationEnded(10);
-        // Second shard failed
-        shard2.testRunStarted(ID, 2);
-        TestDescription test2 = new TestDescription(CLASS, METHOD_2);
-        shard2.testStarted(test2);
-        shard2.testEnded(test2, new HashMap<String, Metric>());
-        shard2.testRunFailed("error");
-        shard2.testRunEnded(10, new HashMap<String, Metric>());
-        shard2.invocationEnded(10);
-
-        IInvocationResult result = mReporter.getResult();
-        assertEquals("Expected 2 pass", 2, result.countResults(TestStatus.PASS));
-        assertEquals("Expected 0 failures", 0, result.countResults(TestStatus.FAIL));
-        List<IModuleResult> modules = result.getModules();
-        assertEquals("Expected 1 module", 1, modules.size());
-        IModuleResult module = modules.get(0);
-
-        // Ensure module is seen as not done and failed
-        assertFalse(module.isDone());
-        assertTrue(module.isFailed());
-
-        assertEquals("Incorrect ID", ID, module.getId());
-        List<ICaseResult> caseResults = module.getResults();
-        assertEquals("Expected 1 test run", 1, caseResults.size());
-        ICaseResult caseResult = caseResults.get(0);
-        List<ITestResult> testResults = caseResult.getResults();
-        assertEquals("Expected 2 test cases", 2, testResults.size());
-        ITestResult result1 = caseResult.getResult(METHOD_1);
-        assertNotNull(String.format("Expected result for %s", TEST_1), result1);
-        assertEquals(
-                String.format("Expected pass for %s", TEST_1),
-                TestStatus.PASS,
-                result1.getResultStatus());
-    }
-
-    /** Ensure that the run history of the current run is added to previous run history. */
-    public void testRetryWithRunHistory() throws Exception {
-        mReporter.invocationStarted(mContext);
-
-        // Set up IInvocationResult with existing results from previous session
-        mReporter.testRunStarted(ID, 2);
-        IInvocationResult invocationResult = mReporter.getResult();
-        IModuleResult moduleResult = invocationResult.getOrCreateModule(ID);
-        ICaseResult caseResult = moduleResult.getOrCreateResult(CLASS);
-        ITestResult testResult1 = caseResult.getOrCreateResult(METHOD_1);
-        testResult1.setResultStatus(TestStatus.PASS);
-        testResult1.setRetry(true);
-        ITestResult testResult2 = caseResult.getOrCreateResult(METHOD_2);
-        testResult2.setResultStatus(TestStatus.FAIL);
-        testResult2.setStackTrace(STACK_TRACE);
-        testResult2.setRetry(true);
-        // Set up IInvocationResult with the run history of previous runs.
-        invocationResult.addInvocationInfo(
-                "run_history", "[{\"startTime\":1,\"endTime\":2},{\"startTime\":3,\"endTime\":4}]");
-
-        // Flip results for the current session
-        TestDescription test1 = new TestDescription(CLASS, METHOD_1);
-        mReporter.testStarted(test1);
-        mReporter.testFailed(test1, STACK_TRACE);
-        mReporter.testEnded(test1, new HashMap<String, Metric>());
-        TestDescription test2 = new TestDescription(CLASS, METHOD_2);
-        mReporter.testStarted(test2);
-        mReporter.testEnded(test2, new HashMap<String, Metric>());
-
-        mReporter.testRunEnded(10, new HashMap<String, Metric>());
-        mReporter.invocationEnded(10);
-
-        // Verification that results have been overwritten.
-        IInvocationResult result = mReporter.getResult();
-        assertEquals("Expected 1 pass", 1, result.countResults(TestStatus.PASS));
-        assertEquals("Expected 1 failure", 1, result.countResults(TestStatus.FAIL));
-        List<IModuleResult> modules = result.getModules();
-        assertEquals("Expected 1 module", 1, modules.size());
-        IModuleResult module = modules.get(0);
-        List<ICaseResult> cases = module.getResults();
-        assertEquals("Expected 1 test case", 1, cases.size());
-        ICaseResult case1 = cases.get(0);
-        List<ITestResult> testResults = case1.getResults();
-        assertEquals("Expected 2 tests", 2, testResults.size());
-
-        long startTime = mReporter.getResult().getStartTime();
-        String expectedRunHistory =
-                String.format(
-                        "[{\"startTime\":1,\"endTime\":2},"
-                                + "{\"startTime\":3,\"endTime\":4},{\"startTime\":%d,\"endTime\":%d}]",
-                        startTime, startTime + 10);
-        assertEquals(expectedRunHistory, invocationResult.getInvocationInfo().get("run_history"));
-
-        // Test 1 details
-        ITestResult finalTestResult1 = case1.getResult(METHOD_1);
-        assertNotNull(String.format("Expected result for %s", TEST_1), finalTestResult1);
-        assertEquals(
-                String.format("Expected fail for %s", TEST_1),
-                TestStatus.FAIL,
-                finalTestResult1.getResultStatus());
-        assertEquals(finalTestResult1.getStackTrace(), STACK_TRACE);
-
-        // Test 2 details
-        ITestResult finalTestResult2 = case1.getResult(METHOD_2);
-        assertNotNull(String.format("Expected result for %s", TEST_2), finalTestResult2);
-        assertEquals(
-                String.format("Expected pass for %s", TEST_2),
-                TestStatus.PASS,
-                finalTestResult2.getResultStatus());
-    }
-}
diff --git a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/suite/PreviousSessionFileCopierTest.java b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/suite/PreviousSessionFileCopierTest.java
deleted file mode 100644
index a0c3131..0000000
--- a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/suite/PreviousSessionFileCopierTest.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.compatibility.common.tradefed.result.suite;
-
-import static org.junit.Assert.assertEquals;
-
-import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
-import com.android.tradefed.build.BuildInfo;
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.config.ConfigurationDef;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.invoker.InvocationContext;
-import com.android.tradefed.util.FileUtil;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-
-/** Unit tests for {@link PreviousSessionFileCopier}. */
-@RunWith(JUnit4.class)
-public class PreviousSessionFileCopierTest {
-
-    private PreviousSessionFileCopier mCopier;
-    private File mPreviousDir;
-    private File mCurrentDir;
-    private IInvocationContext mContext;
-
-    @Before
-    public void setUp() throws Exception {
-        mCurrentDir = FileUtil.createTempDir("current-copier-tests");
-        mCopier =
-                new PreviousSessionFileCopier() {
-                    @Override
-                    protected CompatibilityBuildHelper createCompatibilityHelper(IBuildInfo info) {
-                        return new CompatibilityBuildHelper(info) {
-                            @Override
-                            public File getResultDir() throws FileNotFoundException {
-                                return mCurrentDir;
-                            }
-                        };
-                    }
-                };
-        mPreviousDir = FileUtil.createTempDir("previous-copier-tests");
-        mContext = new InvocationContext();
-        mContext.addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, new BuildInfo());
-        mCopier.setPreviousSessionDir(mPreviousDir);
-    }
-
-    @After
-    public void tearDown() {
-        FileUtil.recursiveDelete(mPreviousDir);
-    }
-
-    @Test
-    public void testCopy() throws Exception {
-        new File(mPreviousDir, "newFile").createNewFile();
-        assertEquals(0, mCurrentDir.listFiles().length);
-        mCopier.invocationStarted(mContext);
-        mCopier.invocationEnded(500L);
-        assertEquals(1, mCurrentDir.listFiles().length);
-    }
-
-    @Test
-    public void testCopy_fileExists() throws Exception {
-        File original = new File(mCurrentDir, "newFile");
-        original.createNewFile();
-        FileUtil.writeToFile("CURRENT", original);
-
-        File previous = new File(mPreviousDir, "newFile");
-        previous.createNewFile();
-        FileUtil.writeToFile("PREVIOUS", previous);
-
-        assertEquals(1, mCurrentDir.listFiles().length);
-        mCopier.invocationStarted(mContext);
-        mCopier.invocationEnded(500L);
-        assertEquals(1, mCurrentDir.listFiles().length);
-        // File are not overriden
-        assertEquals("CURRENT", FileUtil.readStringFromFile(original));
-    }
-
-    @Test
-    public void testCopy_fileExcluded() throws Exception {
-        new File(mPreviousDir, "proto").mkdir();
-        mCopier.invocationStarted(mContext);
-        mCopier.invocationEnded(500L);
-        // Nothing was copied
-        assertEquals(0, mCurrentDir.listFiles().length);
-    }
-}
diff --git a/common_util/com/android/tradefed/config/ConfigurationException.java b/common_util/com/android/tradefed/config/ConfigurationException.java
index 2af9c01..696ee59 100644
--- a/common_util/com/android/tradefed/config/ConfigurationException.java
+++ b/common_util/com/android/tradefed/config/ConfigurationException.java
@@ -15,10 +15,13 @@
  */
 package com.android.tradefed.config;
 
-/**
- * Thrown if configuration could not be loaded.
- */
-public class ConfigurationException extends Exception {
+import com.android.tradefed.error.HarnessException;
+import com.android.tradefed.result.error.ErrorIdentifier;
+
+import java.lang.StackWalker.Option;
+
+/** Thrown if configuration could not be loaded. */
+public class ConfigurationException extends HarnessException {
     private static final long serialVersionUID = 7742154448569011969L;
 
     /**
@@ -27,7 +30,19 @@
      * @param msg a meaningful error message
      */
     public ConfigurationException(String msg) {
-        super(msg);
+        super(msg, null);
+        setCallerClass(StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass());
+    }
+
+    /**
+     * Creates a {@link ConfigurationException}.
+     *
+     * @param msg a meaningful error message
+     * @param error The {@link ErrorIdentifier} associated with the exception
+     */
+    public ConfigurationException(String msg, ErrorIdentifier error) {
+        super(msg, error);
+        setCallerClass(StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass());
     }
 
     /**
@@ -37,7 +52,20 @@
      * @param cause the {@link Throwable} that represents the original cause of the error
      */
     public ConfigurationException(String msg, Throwable cause) {
-        super(msg, cause);
+        super(msg, cause, null);
+        setCallerClass(StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass());
+    }
+
+    /**
+     * Creates a {@link ConfigurationException}.
+     *
+     * @param msg a meaningful error message
+     * @param cause the {@link Throwable} that represents the original cause of the error
+     * @param error The {@link ErrorIdentifier} associated with the exception
+     */
+    public ConfigurationException(String msg, Throwable cause, ErrorIdentifier error) {
+        super(msg, cause, error);
+        setCallerClass(StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass());
     }
 }
 
diff --git a/device_build_interfaces/com/android/tradefed/error/HarnessException.java b/common_util/com/android/tradefed/error/HarnessException.java
similarity index 93%
rename from device_build_interfaces/com/android/tradefed/error/HarnessException.java
rename to common_util/com/android/tradefed/error/HarnessException.java
index 4746671..f6fc35e 100644
--- a/device_build_interfaces/com/android/tradefed/error/HarnessException.java
+++ b/common_util/com/android/tradefed/error/HarnessException.java
@@ -70,4 +70,10 @@
             mOrigin = clazz.getCanonicalName();
         }
     }
+
+    protected final void setCallerClass(String clazz) {
+        if (clazz != null && mOrigin == null) {
+            mOrigin = clazz;
+        }
+    }
 }
diff --git a/device_build_interfaces/com/android/tradefed/error/HarnessRuntimeException.java b/common_util/com/android/tradefed/error/HarnessRuntimeException.java
similarity index 94%
rename from device_build_interfaces/com/android/tradefed/error/HarnessRuntimeException.java
rename to common_util/com/android/tradefed/error/HarnessRuntimeException.java
index 1da5df9..9b88de7 100644
--- a/device_build_interfaces/com/android/tradefed/error/HarnessRuntimeException.java
+++ b/common_util/com/android/tradefed/error/HarnessRuntimeException.java
@@ -78,4 +78,10 @@
     public String getOrigin() {
         return mOrigin;
     }
+
+    protected final void setCallerClass(Class<?> clazz) {
+        if (clazz != null) {
+            mOrigin = clazz.getCanonicalName();
+        }
+    }
 }
diff --git a/device_build_interfaces/com/android/tradefed/error/IHarnessException.java b/common_util/com/android/tradefed/error/IHarnessException.java
similarity index 100%
rename from device_build_interfaces/com/android/tradefed/error/IHarnessException.java
rename to common_util/com/android/tradefed/error/IHarnessException.java
diff --git a/common_util/com/android/tradefed/result/LogDataType.java b/common_util/com/android/tradefed/result/LogDataType.java
index da1eb9e..0bcb9a8 100644
--- a/common_util/com/android/tradefed/result/LogDataType.java
+++ b/common_util/com/android/tradefed/result/LogDataType.java
@@ -32,7 +32,7 @@
     JPEG("jpeg", "image/jpeg", true, false),
     TAR_GZ("tar.gz", "application/gzip", true, false),
     GZIP("gz", "application/gzip", true, false),
-    HPROF("hprof", "text/plain", true, false),
+    HPROF("hprof", "application/octet-stream", true, false),
     COVERAGE("ec", "text/plain", false, false), // Emma coverage file
     NATIVE_COVERAGE("zip", "application/zip", true, false), // gcov coverage archive
     CLANG_COVERAGE("profdata", "text/plain", false, false), // LLVM indexed profile data
@@ -60,6 +60,7 @@
     ATRACE("atr", "text/plain", true, false), // atrace -z format
     KERNEL_TRACE("dat", "text/plain", false, false), // raw kernel ftrace buffer
     DIR("", "text/plain", false, false),
+    CFG("cfg", "application/octet-stream", false, true),
     /* Unknown file type */
     UNKNOWN("dat", "text/plain", false, false);
 
diff --git a/common_util/com/android/tradefed/result/error/DeviceErrorIdentifier.java b/common_util/com/android/tradefed/result/error/DeviceErrorIdentifier.java
new file mode 100644
index 0000000..483d02b
--- /dev/null
+++ b/common_util/com/android/tradefed/result/error/DeviceErrorIdentifier.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.result.error;
+
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
+
+/** Error Identifiers from Device errors and device reported errors. */
+public enum DeviceErrorIdentifier implements ErrorIdentifier {
+
+    // ********************************************************************************************
+    // Device Errors: 520_001 ~ 530_000
+    // ********************************************************************************************
+    APK_INSTALLATION_FAILED(520_001, FailureStatus.DEPENDENCY_ISSUE),
+    FAIL_ACTIVATE_APEX(520_002, FailureStatus.DEPENDENCY_ISSUE),
+
+    AAPT_PARSER_FAILED(520_050, FailureStatus.DEPENDENCY_ISSUE),
+
+    SHELL_COMMAND_ERROR(520_100, FailureStatus.DEPENDENCY_ISSUE),
+    DEVICE_UNEXPECTED_RESPONSE(30_101, FailureStatus.DEPENDENCY_ISSUE),
+    FAIL_PUSH_FILE(30_102, FailureStatus.DEPENDENCY_ISSUE),
+    FAIL_PULL_FILE(30_103, FailureStatus.DEPENDENCY_ISSUE),
+    DEVICE_FAILED_TO_RESET(30_104, FailureStatus.DEPENDENCY_ISSUE),
+
+    INSTRUMENTATION_CRASH(520_200, FailureStatus.SYSTEM_UNDER_TEST_CRASHED),
+
+    FAILED_TO_LAUNCH_GCE(520_500, FailureStatus.LOST_SYSTEM_UNDER_TEST),
+    FAILED_TO_CONNECT_TO_GCE(520_501, FailureStatus.LOST_SYSTEM_UNDER_TEST),
+    ERROR_AFTER_FLASHING(520_502, FailureStatus.LOST_SYSTEM_UNDER_TEST),
+
+    DEVICE_UNAVAILABLE(520_750, FailureStatus.LOST_SYSTEM_UNDER_TEST),
+    DEVICE_UNRESPONSIVE(520_751, FailureStatus.LOST_SYSTEM_UNDER_TEST);
+
+    private final long code;
+    private final FailureStatus status;
+
+    DeviceErrorIdentifier(int code, FailureStatus status) {
+        this.code = code;
+        this.status = status;
+    }
+
+    @Override
+    public long code() {
+        return code;
+    }
+
+    @Override
+    public FailureStatus status() {
+        return status;
+    }
+}
diff --git a/test_result_interfaces/com/android/tradefed/result/error/ErrorIdentifier.java b/common_util/com/android/tradefed/result/error/ErrorIdentifier.java
similarity index 92%
rename from test_result_interfaces/com/android/tradefed/result/error/ErrorIdentifier.java
rename to common_util/com/android/tradefed/result/error/ErrorIdentifier.java
index 4d00bdd..48e0b40 100644
--- a/test_result_interfaces/com/android/tradefed/result/error/ErrorIdentifier.java
+++ b/common_util/com/android/tradefed/result/error/ErrorIdentifier.java
@@ -15,7 +15,6 @@
  */
 package com.android.tradefed.result.error;
 
-import com.android.tradefed.result.FailureDescription;
 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 
 /**
@@ -33,7 +32,7 @@
 
     /**
      * The failure status associated with the identifier, this status is expected to align with the
-     * {@link FailureDescription} one.
+     * FailureDescription one.
      */
     public FailureStatus status();
 }
diff --git a/common_util/com/android/tradefed/result/error/InfraErrorIdentifier.java b/common_util/com/android/tradefed/result/error/InfraErrorIdentifier.java
new file mode 100644
index 0000000..619b4fb
--- /dev/null
+++ b/common_util/com/android/tradefed/result/error/InfraErrorIdentifier.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.result.error;
+
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
+
+/** Error Identifiers from Trade Federation infra, and dependent infra (like Build infra). */
+public enum InfraErrorIdentifier implements ErrorIdentifier {
+
+    // ********************************************************************************************
+    // Infra: 500_001 ~ 510_000
+    // ********************************************************************************************
+    // 500_001 - 500_500: General errors
+    ARTIFACT_NOT_FOUND(500_001, FailureStatus.DEPENDENCY_ISSUE),
+    FAIL_TO_CREATE_FILE(500_002, FailureStatus.INFRA_FAILURE),
+    INVOCATION_CANCELLED(500_003, FailureStatus.CANCELLED),
+    CODE_COVERAGE_ERROR(500_004, FailureStatus.INFRA_FAILURE),
+    MODULE_SETUP_RUNTIME_EXCEPTION(500_005, FailureStatus.CUSTOMER_ISSUE),
+    CONFIGURED_ARTIFACT_NOT_FOUND(500_006, FailureStatus.CUSTOMER_ISSUE),
+    INVOCATION_TIMEOUT(500_007, FailureStatus.TIMED_OUT),
+    OPTION_CONFIGURATION_ERROR(500_008, FailureStatus.CUSTOMER_ISSUE),
+    RUNNER_ALLOCATION_ERROR(500_009, FailureStatus.INFRA_FAILURE),
+    SCHEDULER_ALLOCATION_ERROR(500_010, FailureStatus.CUSTOMER_ISSUE),
+
+    // 500_501 - 501_000: Build, Artifacts download related errors
+    ARTIFACT_REMOTE_PATH_NULL(500_501, FailureStatus.INFRA_FAILURE),
+    ARTIFACT_UNSUPPORTED_PATH(500_502, FailureStatus.INFRA_FAILURE),
+    ARTIFACT_DOWNLOAD_ERROR(500_503, FailureStatus.DEPENDENCY_ISSUE),
+    GCS_ERROR(500_504, FailureStatus.DEPENDENCY_ISSUE),
+
+    // 501_001 - 501_500: environment issues: For example: lab wifi
+    WIFI_FAILED_CONNECT(501_001, FailureStatus.DEPENDENCY_ISSUE),
+    GOOGLE_ACCOUNT_SETUP_FAILED(501_002, FailureStatus.DEPENDENCY_ISSUE),
+    NO_WIFI(501_003, FailureStatus.DEPENDENCY_ISSUE),
+
+    // 502_000 - 502_100: Test issues detected by infra
+    EXPECTED_TESTS_MISMATCH(502_000, FailureStatus.TEST_FAILURE),
+
+    // 505_000 - 505_250: Acloud errors
+    NO_ACLOUD_REPORT(505_000, FailureStatus.DEPENDENCY_ISSUE),
+
+    UNDETERMINED(510_000, FailureStatus.UNSET);
+
+    private final long code;
+    private final FailureStatus status;
+
+    InfraErrorIdentifier(int code, FailureStatus status) {
+        this.code = code;
+        this.status = status;
+    }
+
+    @Override
+    public long code() {
+        return code;
+    }
+
+    @Override
+    public FailureStatus status() {
+        return status;
+    }
+}
diff --git a/common_util/com/android/tradefed/result/error/TestErrorIdentifier.java b/common_util/com/android/tradefed/result/error/TestErrorIdentifier.java
new file mode 100644
index 0000000..5f80ba4
--- /dev/null
+++ b/common_util/com/android/tradefed/result/error/TestErrorIdentifier.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.result.error;
+
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
+
+/** Error identifier from tests and tests runners. */
+public enum TestErrorIdentifier implements ErrorIdentifier {
+    MODULE_DID_NOT_EXECUTE(530_001, FailureStatus.NOT_EXECUTED);
+
+    private final long code;
+    private final FailureStatus status;
+
+    TestErrorIdentifier(int code, FailureStatus status) {
+        this.code = code;
+        this.status = status;
+    }
+
+    @Override
+    public long code() {
+        return code;
+    }
+
+    @Override
+    public FailureStatus status() {
+        return status;
+    }
+}
diff --git a/common_util/com/android/tradefed/util/Email.java b/common_util/com/android/tradefed/util/Email.java
index 879e7b5..2c2c79f 100644
--- a/common_util/com/android/tradefed/util/Email.java
+++ b/common_util/com/android/tradefed/util/Email.java
@@ -94,7 +94,7 @@
      */
     @Override
     public void send(Message msg) throws IllegalArgumentException, IOException {
-        // Sanity checks
+        // Validity checks
         if (msg.getTo() == null) {
             throw new IllegalArgumentException("Message is missing a destination");
         } else if (msg.getSubject() == null) {
diff --git a/common_util/com/android/tradefed/util/FileUtil.java b/common_util/com/android/tradefed/util/FileUtil.java
index 70ee95c..017224d 100644
--- a/common_util/com/android/tradefed/util/FileUtil.java
+++ b/common_util/com/android/tradefed/util/FileUtil.java
@@ -243,10 +243,8 @@
     protected static boolean chmodExists() {
         // Silence the scary process exception when chmod is missing, we will log instead.
         CommandResult result = RunUtil.getDefault().runTimedCmdSilently(10 * 1000, sChmod);
-        // We expect a status fail because 'chmod' requires arguments.
-        String stderr = result.getStderr();
-        if (CommandStatus.FAILED.equals(result.getStatus()) &&
-                (stderr.contains("chmod: missing operand") || stderr.contains("usage: "))) {
+        // Exit code 127 means “command not found”.
+        if (result.getExitCode() != null && result.getExitCode() != 127) {
             return true;
         }
         CLog.w("Chmod is not supported by this OS.");
diff --git a/common_util/com/android/tradefed/util/SparseImageUtil.java b/common_util/com/android/tradefed/util/SparseImageUtil.java
new file mode 100644
index 0000000..ce557a5
--- /dev/null
+++ b/common_util/com/android/tradefed/util/SparseImageUtil.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+/**
+ * Utility to unsparse sparse images.
+ *
+ * <p>This piece of code is adopted from:
+ * frameworks/base/packages/DynamicSystemInstallationService/src/com/android/dynsystem/SparseInputStream.java
+ */
+public class SparseImageUtil {
+    private static final int SPARSE_IMAGE_MAGIC = 0xED26FF3A;
+
+    /**
+     * Tests if file is a sparse image.
+     *
+     * @param imgFile a {@link File} that is to be tested.
+     * @return true if imgFile is a sparse image.
+     */
+    public static boolean isSparse(File imgFile) {
+        if (!imgFile.isFile()) {
+            return false;
+        }
+        try (FileInputStream in = new FileInputStream(imgFile)) {
+            // Check magic bytes
+            return readBuffer(in, 4).getInt() == SPARSE_IMAGE_MAGIC;
+        } catch (IOException e) {
+            // Return false if failed to read file
+            return false;
+        }
+    }
+
+    /**
+     * Unsparses a sparse image file.
+     *
+     * @param imgFile a {@link File} that is a sparse image.
+     * @param destFile a {@link File} to write the unsparsed image to.
+     * @throws IOException if imgFile is not a sparse image.
+     */
+    public static void unsparse(File imgFile, File destFile) throws IOException {
+        try (FileInputStream in = new FileInputStream(imgFile)) {
+            SparseInputStream sis = new SparseInputStream(new BufferedInputStream(in));
+            if (!sis.isSparse()) {
+                throw new IOException("Not a sparse image: " + imgFile);
+            }
+            FileUtil.writeToFile(sis, destFile);
+        }
+    }
+
+    /** Reads exact number of bytes. */
+    private static byte[] readFully(InputStream in, int size) throws IOException {
+        byte[] buf = new byte[size];
+        int n = 0;
+        int off = 0;
+        int left = size;
+        while (left > 0) {
+            n = in.read(buf, off, left);
+            if (n < 0) {
+                throw new IOException("Unexpected EOF in readFully()");
+            }
+            off += n;
+            left -= n;
+        }
+        return buf;
+    }
+
+    /** Helper that wraps result of readFully() in a ByteBuffer for easy consumption. */
+    private static ByteBuffer readBuffer(InputStream in, int size) throws IOException {
+        return ByteBuffer.wrap(readFully(in, size)).order(ByteOrder.LITTLE_ENDIAN);
+    }
+
+    /**
+     * SparseInputStream read from upstream and detects the data format. If the upstream is a valid
+     * sparse data, it will unsparse it on the fly. Otherwise, it just passthrough as is.
+     */
+    private static class SparseInputStream extends InputStream {
+        private static final int FILE_HDR_SIZE = 28;
+        private static final int CHUNK_HDR_SIZE = 12;
+
+        /**
+         * This class represents a chunk in the Android sparse image.
+         *
+         * @see system/core/libsparse/sparse_format.h
+         */
+        private static class SparseChunk {
+            public static final short RAW = (short) 0xCAC1;
+            public static final short FILL = (short) 0xCAC2;
+            public static final short DONTCARE = (short) 0xCAC3;
+            public short mChunkType;
+            public int mChunkSize;
+            public int mTotalSize;
+            public byte[] mFill;
+
+            @Override
+            public String toString() {
+                return String.format(
+                        "type: %x, chunk_size: %d, total_size: %d",
+                        mChunkType, mChunkSize, mTotalSize);
+            }
+
+            public static SparseChunk readChunk(InputStream in) throws IOException {
+                SparseChunk chunk = new SparseChunk();
+                ByteBuffer buf = readBuffer(in, CHUNK_HDR_SIZE);
+                chunk.mChunkType = buf.getShort();
+                /* padding = */ buf.getShort();
+                chunk.mChunkSize = buf.getInt();
+                chunk.mTotalSize = buf.getInt();
+                if (chunk.mChunkType == FILL) {
+                    chunk.mFill = readFully(in, 4);
+                }
+                return chunk;
+            }
+        }
+
+        private BufferedInputStream mIn;
+        private boolean mIsSparse;
+        private long mBlockSize;
+        private long mTotalBlocks;
+        private long mTotalChunks;
+        private SparseChunk mCur;
+        private long mLeft;
+        private int mCurChunks;
+
+        public SparseInputStream(BufferedInputStream in) throws IOException {
+            mIn = in;
+            in.mark(FILE_HDR_SIZE * 2);
+            ByteBuffer buf = readBuffer(mIn, FILE_HDR_SIZE);
+            mIsSparse = (buf.getInt() == SPARSE_IMAGE_MAGIC);
+            if (!mIsSparse) {
+                mIn.reset();
+                return;
+            }
+            int major = buf.getShort();
+            int minor = buf.getShort();
+            if (major > 0x1 || minor > 0x0) {
+                throw new IOException("Unsupported sparse version: " + major + "." + minor);
+            }
+            if (buf.getShort() != FILE_HDR_SIZE) {
+                throw new IOException("Illegal file header size");
+            }
+            if (buf.getShort() != CHUNK_HDR_SIZE) {
+                throw new IOException("Illegal chunk header size");
+            }
+            mBlockSize = buf.getInt();
+            if ((mBlockSize & 0x3) != 0) {
+                throw new IOException("Illegal block size, must be a multiple of 4: " + mBlockSize);
+            }
+            mTotalBlocks = buf.getInt();
+            mTotalChunks = buf.getInt();
+            mLeft = 0;
+            mCurChunks = 0;
+        }
+
+        /**
+         * Check if it needs to open a new chunk.
+         *
+         * @return true if it's EOF
+         */
+        private boolean prepareChunk() throws IOException {
+            if (mCur == null || mLeft <= 0) {
+                if (++mCurChunks > mTotalChunks) {
+                    return true;
+                }
+                mCur = SparseChunk.readChunk(mIn);
+                mLeft = mCur.mChunkSize * mBlockSize;
+            }
+            return mLeft == 0;
+        }
+
+        @Override
+        public int read(byte[] buf, int off, int len) throws IOException {
+            if (!mIsSparse) {
+                return mIn.read(buf, off, len);
+            }
+            if (prepareChunk()) {
+                return -1;
+            }
+            int n = -1;
+            switch (mCur.mChunkType) {
+                case SparseChunk.RAW:
+                    n = mIn.read(buf, off, (int) Math.min(mLeft, len));
+                    mLeft -= n;
+                    break;
+                case SparseChunk.DONTCARE:
+                    n = (int) Math.min(mLeft, len);
+                    Arrays.fill(buf, off, off + n, (byte) 0);
+                    mLeft -= n;
+                    break;
+                case SparseChunk.FILL:
+                    // The FILL type is rarely used, so use a simple implementation.
+                    n = super.read(buf, off, len);
+                    break;
+                default:
+                    throw new IOException("Unsupported Chunk:" + mCur);
+            }
+            return n;
+        }
+
+        @Override
+        public int read() throws IOException {
+            if (!mIsSparse) {
+                return mIn.read();
+            }
+            if (prepareChunk()) {
+                return -1;
+            }
+            int ret = -1;
+            switch (mCur.mChunkType) {
+                case SparseChunk.RAW:
+                    ret = mIn.read();
+                    break;
+                case SparseChunk.DONTCARE:
+                    ret = 0;
+                    break;
+                case SparseChunk.FILL:
+                    ret = mCur.mFill[(4 - ((int) mLeft & 0x3)) & 0x3];
+                    break;
+                default:
+                    throw new IOException("Unsupported Chunk:" + mCur);
+            }
+            mLeft--;
+            return ret;
+        }
+
+        /**
+         * Get the unsparse size
+         *
+         * @return -1 if stream doesn't contain sparse image data.
+         */
+        public long getUnsparseSize() {
+            if (!mIsSparse) {
+                return -1;
+            }
+            return mBlockSize * mTotalBlocks;
+        }
+
+        public boolean isSparse() {
+            return mIsSparse;
+        }
+    }
+}
diff --git a/common_util/com/android/tradefed/util/ZipUtil.java b/common_util/com/android/tradefed/util/ZipUtil.java
index 31f3ff8..a6902ec 100644
--- a/common_util/com/android/tradefed/util/ZipUtil.java
+++ b/common_util/com/android/tradefed/util/ZipUtil.java
@@ -526,6 +526,7 @@
                 return;
             } else if (zipEntry.getCompressedSize() == 0) {
                 // The file is empty, just create an empty file.
+                FileUtil.mkdirsRWX(targetFile.getParentFile());
                 targetFile.createNewFile();
                 return;
             }
diff --git a/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java b/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java
index fc2687c..9638057 100644
--- a/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java
+++ b/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java
@@ -16,7 +16,6 @@
 
 package com.android.tradefed.util.zip;
 
-import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.ByteArrayUtil;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -241,9 +240,6 @@
         if (Long.toHexString(mUncompressedSize).equals("ffffffff") ||
             Long.toHexString(mCompressedSize).equals("ffffffff") ||
             Long.toHexString(mLocalHeaderOffset).equals("ffffffff")) {
-            CLog.i("Values(compressed/uncompressed size, and relative offset of local header)) in "
-                    + "CentralDirectoryInfo for file name: %s reaches the limitation(0xffffffff), "
-                    + "getting the data from extra field.", mFileName);
             byte[] zip64FieldId = Arrays.copyOfRange(
                     data, startOffset + mFileNameLength + 46, startOffset + mFileNameLength + 48);
             // There should be a ZIP64 Field ID(0x0001) existing here.
diff --git a/device_build_interfaces/com/android/tradefed/device/DeviceRuntimeException.java b/device_build_interfaces/com/android/tradefed/device/DeviceRuntimeException.java
index 1e853c8..4a996f4 100644
--- a/device_build_interfaces/com/android/tradefed/device/DeviceRuntimeException.java
+++ b/device_build_interfaces/com/android/tradefed/device/DeviceRuntimeException.java
@@ -15,31 +15,29 @@
  */
 package com.android.tradefed.device;
 
+import com.android.tradefed.error.HarnessRuntimeException;
+import com.android.tradefed.result.error.ErrorIdentifier;
+
+import java.lang.StackWalker.Option;
+
 /**
  * Thrown when a device action did not results in the expected results.
  *
  * <p>For example: 'pm list users' is vastly expected to return the list of users, failure to do so
  * should be raised as a DeviceRuntimeException since something went very wrong.
  */
-public class DeviceRuntimeException extends RuntimeException {
+public class DeviceRuntimeException extends HarnessRuntimeException {
     private static final long serialVersionUID = -7928528651742852301L;
 
     /**
      * Creates a {@link DeviceRuntimeException}.
      *
      * @param msg a descriptive error message of the error.
+     * @param errorId The {@link ErrorIdentifier} categorizing the exception.
      */
-    public DeviceRuntimeException(String msg) {
-        super(msg);
-    }
-
-    /**
-     * Creates a {@link DeviceRuntimeException}.
-     *
-     * @param t {@link Throwable} that should be wrapped in {@link DeviceRuntimeException}.
-     */
-    public DeviceRuntimeException(Throwable t) {
-        super(t);
+    public DeviceRuntimeException(String msg, ErrorIdentifier errorId) {
+        super(msg, errorId);
+        setCallerClass(StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass());
     }
 
     /**
@@ -47,8 +45,10 @@
      *
      * @param msg a descriptive error message of the error
      * @param t {@link Throwable} that should be wrapped in {@link DeviceRuntimeException}.
+     * @param errorId The {@link ErrorIdentifier} categorizing the exception.
      */
-    public DeviceRuntimeException(String msg, Throwable t) {
-        super(msg, t);
+    public DeviceRuntimeException(String msg, Throwable t, ErrorIdentifier errorId) {
+        super(msg, t, errorId);
+        setCallerClass(StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass());
     }
 }
diff --git a/device_build_interfaces/com/android/tradefed/device/contentprovider/ContentProviderHandler.java b/device_build_interfaces/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
index 3e37f20..3163b52 100644
--- a/device_build_interfaces/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
+++ b/device_build_interfaces/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
@@ -244,6 +244,29 @@
         return false;
     }
 
+    /**
+     * Determines if the file or non-empty directory exists on the device.
+     *
+     * @param deviceFilePath The absolute file path on device to check for existence.
+     * @return True if file/directory exists, False otherwise. If directory is empty, it will return
+     *     False as well.
+     */
+    public boolean doesFileExist(String deviceFilePath) throws DeviceNotAvailableException {
+        String contentUri = createEscapedContentUri(deviceFilePath);
+        String queryContentCommand =
+                String.format(
+                        "content query --user %d --uri %s", mDevice.getCurrentUser(), contentUri);
+
+        String listCommandResult = mDevice.executeShellCommand(queryContentCommand);
+
+        if (NO_RESULTS_STRING.equals(listCommandResult.trim())) {
+            // No file found.
+            return false;
+        }
+
+        return true;
+    }
+
     /** Returns true if {@link CommandStatus} is successful and there is no error message. */
     private boolean isSuccessful(CommandResult result) {
         if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
diff --git a/invocation_interfaces/com/android/tradefed/invoker/logger/CurrentInvocation.java b/invocation_interfaces/com/android/tradefed/invoker/logger/CurrentInvocation.java
index e40e7b5..18cbe19 100644
--- a/invocation_interfaces/com/android/tradefed/invoker/logger/CurrentInvocation.java
+++ b/invocation_interfaces/com/android/tradefed/invoker/logger/CurrentInvocation.java
@@ -174,6 +174,7 @@
         }
         if (errorIdentifier != null) {
             failure.setErrorIdentifier(errorIdentifier);
+            failure.setFailureStatus(errorIdentifier.status());
         }
         // Automatically populate the origin
         Class<?> clazz = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass();
diff --git a/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java b/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
index caa5f1e..50084c4 100644
--- a/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
+++ b/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
@@ -47,7 +47,13 @@
         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);
+        CF_INSTANCE_COUNT("cf_instance_count", false),
+        CRASH_FAILURES("crash_failures", true),
+        UNCAUGHT_CRASH_FAILURES("uncaught_crash_failures", true),
+        TEST_CRASH_FAILURES("test_crash_failures", true),
+        UNCAUGHT_TEST_CRASH_FAILURES("uncaught_test_crash_failures", true),
+        DEVICE_RESET_COUNT("device_reset_count", true),
+        DEVICE_RESET_MODULES("device_reset_modules", true);
 
         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/TestResult.java b/invocation_interfaces/com/android/tradefed/result/TestResult.java
index 06d47ad..1b26db6 100644
--- a/invocation_interfaces/com/android/tradefed/result/TestResult.java
+++ b/invocation_interfaces/com/android/tradefed/result/TestResult.java
@@ -208,6 +208,7 @@
         int ignored = 0;
         int incomplete = 0;
 
+        TestStatus lastStatus = null;
         for (TestResult attempt : results) {
             mergedResult.mProtoMetrics.putAll(attempt.getProtoMetrics());
             mergedResult.mMetrics.putAll(attempt.getMetrics());
@@ -238,6 +239,7 @@
                     ignored++;
                     break;
             }
+            lastStatus = attempt.getStatus();
         }
 
         switch (strategy) {
@@ -258,11 +260,17 @@
                         mergedResult.setStatus(TestStatus.INCOMPLETE);
                     }
                 } else {
-                    mergedResult.setStatus(TestStatus.FAILURE);
+                    if (TestStatus.ASSUMPTION_FAILURE.equals(lastStatus)) {
+                        mergedResult.setStatus(TestStatus.ASSUMPTION_FAILURE);
+                    } else if (TestStatus.IGNORED.equals(lastStatus)) {
+                        mergedResult.setStatus(TestStatus.IGNORED);
+                    } else {
+                        mergedResult.setStatus(TestStatus.FAILURE);
+                    }
                 }
                 break;
             default:
-                // We keep a sane default of one failure is a failure that should be reported.
+                // We keep a default of one failure is a failure that should be reported.
                 if (fail > 0) {
                     mergedResult.setStatus(TestStatus.FAILURE);
                 } else {
diff --git a/invocation_interfaces/com/android/tradefed/result/TestRunResult.java b/invocation_interfaces/com/android/tradefed/result/TestRunResult.java
index a3fdc1e..3f35209 100644
--- a/invocation_interfaces/com/android/tradefed/result/TestRunResult.java
+++ b/invocation_interfaces/com/android/tradefed/result/TestRunResult.java
@@ -307,6 +307,10 @@
         updateTestResult(test, TestStatus.ASSUMPTION_FAILURE, FailureDescription.create(trace));
     }
 
+    public void testAssumptionFailure(TestDescription test, FailureDescription failure) {
+        updateTestResult(test, TestStatus.ASSUMPTION_FAILURE, failure);
+    }
+
     public void testIgnored(TestDescription test) {
         updateTestResult(test, TestStatus.IGNORED, null);
     }
diff --git a/proto/monitoring/server/lab_resource.proto b/proto/monitoring/server/lab_resource.proto
new file mode 100644
index 0000000..5397abe
--- /dev/null
+++ b/proto/monitoring/server/lab_resource.proto
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// The protobuf messages for lab host to export metadata and reosurce metrics.
+syntax = "proto3";
+
+package dual_home_lab.monitoring_agent.resource_monitoring;
+
+import "google/protobuf/timestamp.proto";
+
+option java_package = "com.google.dualhomelab.monitoringagent.resourcemonitoring";
+option java_multiple_files = true;
+option java_generic_services = true;
+
+// A tag-value pair message represents the metric value.
+// For example:
+// To represent device disk used percentage.
+// metric {
+//   tag: "/data"
+//   value: 20.5
+// }
+message Metric {
+  // A string tag associates to the value.
+  string tag = 1;
+  // A float value represents the resource value.
+  float value = 2;
+}
+
+// A message that describes the resource and its metrics.
+// For example:
+// To represent disk space usage values at certain moment.
+// resource {
+//   resource_name: "disk_space"
+//   resource_instance: "/data"
+//   timestamp {
+//     seconds: 1589342214
+//   }
+//   metric: {
+//     tag: "avail"
+//     value: 20.5
+//   }
+//   metric: {
+//     tag: "used"
+//     value: 18.7
+//   }
+//   metric: {
+//     tag: "reserved for root"
+//     value: 16.2
+//   }
+// }
+message Resource {
+  // A string resource name. ex. "cpu", "memory", "disk_space".
+  string resource_name = 1;
+  // A string reperesent the sub resource name.
+  string resource_instance = 2;
+  // The Metric describe the host or device resource usages.
+  repeated Metric metric = 3;
+  // The collecting timestamp.
+  google.protobuf.Timestamp timestamp = 4;
+}
+
+// A name-value message to represent the metadata attribute.
+// For example:
+// To represent the run target.
+// attribute {
+//   name: "run_target"
+//   value: "atom-userdebug"
+// }
+// To reperent the pools.
+// attribute {
+//   name: "pool"
+//   value: "asit"
+// }
+// attribute {
+//   name: "pool"
+//   value: "apct"
+// }
+message Attribute {
+  string name = 1;
+  string value = 2;
+}
+
+// A message that describes the device state and resource usages.
+// For example:
+// To represent a monitored host
+// host {
+//   identifier: {
+//     key: "lab_name"
+//     value: "us-mtv43"
+//   }
+//   identifier: {
+//     key: "host_name"
+//     value: "foo.bar.com"
+//   }
+//   identifier: {
+//     key: "test_harness"
+//     value: "tradefed"
+//   }
+//   attribute: {... check the attribute example above ...}
+//   resource: {... check the resource example abobe ...}
+// }
+// To represent a monitored device
+// device {
+//   identifier: {
+//     key: "device_serial"
+//     value: "VVEG-GIDSAN"
+//   }
+//   attribute: {... check the attribute example above ...}
+//   resource: {... check the resource example abobe ...}
+// }
+message MonitoredEntity {
+  // The string map that helps identify the monitored entity
+  map<string, string> identifier = 1;
+  // The attribute messages that describe the device metadata.
+  repeated Attribute attribute = 2;
+  // The resource messages that describe the device state and resource metrics.
+  repeated Resource resource = 3;
+}
+
+// A message that describe the state and resource usages for a lab host and its
+// connected devices.
+message LabResource {
+  MonitoredEntity host = 1;
+  repeated MonitoredEntity device = 2;
+}
+
+// The request message to query the LabResource.
+message LabResourceRequest {}
+
+// The service which is intend to export device metrics and metadata to the host
+// monitoring agent. The host monitoring agent is responsible for
+// collecting host/device metrics and exporting the metrics to user defined
+// cloud PubSub topics.
+service LabResourceService {
+  // Queries lab resource message.
+  rpc GetLabResource(LabResourceRequest) returns (LabResource) {
+    // The http equivalent is curl http://ADDRESS/v1/lab_resource_message
+    // (Assuming your service is hosted at the given 'ADDRESS')
+  }
+}
diff --git a/res/config/template/preparers/dsu-preparer.xml b/res/config/template/preparers/dsu-preparer.xml
index 565b609..7e716ce 100644
--- a/res/config/template/preparers/dsu-preparer.xml
+++ b/res/config/template/preparers/dsu-preparer.xml
@@ -1,5 +1,21 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright 2020 Google Inc. All Rights Reserved -->
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
 <configuration description="Common template preparer for dynamic system upgrade (DSU)." >
+    <template-include name="base-preparer" default="empty" />
     <target_preparer class="com.android.tradefed.targetprep.DynamicSystemPreparer" />
+    <!-- Can use template/preparers/gki-device-flash-preparer for extra GKI flash-->
+    <template-include name="gki-preparer" default="empty" />
 </configuration>
diff --git a/res/config/template/preparers/fastboot-flash-preparer.xml b/res/config/template/preparers/fastboot-flash-preparer.xml
new file mode 100644
index 0000000..74541de
--- /dev/null
+++ b/res/config/template/preparers/fastboot-flash-preparer.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Common template for flashing the device." >
+    <target_preparer class="com.android.tradefed.targetprep.FastbootUpdateBootstrapPreparer" />
+</configuration>
diff --git a/res/config/template/preparers/gki-device-flash-preparer.xml b/res/config/template/preparers/gki-device-flash-preparer.xml
index dfeffa9..59758ba 100644
--- a/res/config/template/preparers/gki-device-flash-preparer.xml
+++ b/res/config/template/preparers/gki-device-flash-preparer.xml
@@ -1,5 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright 2020 Google Inc. All Rights Reserved -->
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
 <configuration description="Common template for flashing the device with GKI image." >
     <target_preparer class="com.android.tradefed.targetprep.GkiDeviceFlashPreparer" />
 </configuration>
diff --git a/res/config/template/preparers/gsi-device-flash-preparer.xml b/res/config/template/preparers/gsi-device-flash-preparer.xml
index 6c86c49..6121ea5 100644
--- a/res/config/template/preparers/gsi-device-flash-preparer.xml
+++ b/res/config/template/preparers/gsi-device-flash-preparer.xml
@@ -1,5 +1,19 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright 2020 Google Inc. All Rights Reserved -->
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
 <configuration description="Common template for flashing the device with GSI image." >
+    <template-include name="base-preparer" default="empty" />
     <target_preparer class="com.android.tradefed.targetprep.GsiDeviceFlashPreparer" />
 </configuration>
diff --git a/res/suite/allowed-preparers.txt b/res/suite/allowed-preparers.txt
new file mode 100644
index 0000000..48ed998
--- /dev/null
+++ b/res/suite/allowed-preparers.txt
@@ -0,0 +1,4 @@
+com.android.tradefed.targetprep.CrashCollector
+com.android.tradefed.targetprep.DeviceCleaner
+com.android.tradefed.targetprep.RootTargetPreparer
+com.android.tradefed.targetprep.WifiPreparer
\ No newline at end of file
diff --git a/src/com/android/tradefed/build/BuildRetrievalError.java b/src/com/android/tradefed/build/BuildRetrievalError.java
index 56e6921..fc5e4ce 100644
--- a/src/com/android/tradefed/build/BuildRetrievalError.java
+++ b/src/com/android/tradefed/build/BuildRetrievalError.java
@@ -16,8 +16,10 @@
 package com.android.tradefed.build;
 
 import com.android.tradefed.error.HarnessException;
+import com.android.tradefed.error.IHarnessException;
 import com.android.tradefed.result.error.ErrorIdentifier;
 
+
 /** A fatal error occurred while retrieving the build for testing. */
 public class BuildRetrievalError extends HarnessException {
 
@@ -92,6 +94,9 @@
         if (build != null) {
             mBuildInfo = build;
         }
+        if (cause instanceof IHarnessException) {
+            setCallerClass(((IHarnessException) cause).getOrigin());
+        }
     }
 
     /**
diff --git a/src/com/android/tradefed/build/DependenciesResolver.java b/src/com/android/tradefed/build/DependenciesResolver.java
index 66ccd89..f55d098 100644
--- a/src/com/android/tradefed/build/DependenciesResolver.java
+++ b/src/com/android/tradefed/build/DependenciesResolver.java
@@ -59,7 +59,7 @@
     @Option(name = "protocol")
     private String mProtocol = null;
 
-    @Option(name = "use-build-api ")
+    @Option(name = "use-build-api")
     private boolean mUseBuildApi = true;
 
     private File mTestsDir;
diff --git a/src/com/android/tradefed/build/FileDownloadCache.java b/src/com/android/tradefed/build/FileDownloadCache.java
index 357eccc..7cbd297 100644
--- a/src/com/android/tradefed/build/FileDownloadCache.java
+++ b/src/com/android/tradefed/build/FileDownloadCache.java
@@ -401,7 +401,7 @@
 
     @VisibleForTesting
     File copyFile(String remotePath, File cachedFile, File destFile) throws BuildRetrievalError {
-        // attempt to create a local copy of cached file with sane name
+        // attempt to create a local copy of cached file with meaningful name
         File hardlinkFile = destFile;
         try {
             if (hardlinkFile == null) {
diff --git a/src/com/android/tradefed/cluster/ClusterClient.java b/src/com/android/tradefed/cluster/ClusterClient.java
index b9d7fd2..7d006a5 100644
--- a/src/com/android/tradefed/cluster/ClusterClient.java
+++ b/src/com/android/tradefed/cluster/ClusterClient.java
@@ -214,6 +214,11 @@
 
     @Override
     public ClusterCommand.State getCommandState(String requestId, String commandId) {
+        return getCommandStatus(requestId, commandId).getState();
+    }
+
+    @Override
+    public ClusterCommandStatus getCommandStatus(String requestId, String commandId) {
         try {
             HttpResponse response =
                     getApiHelper()
@@ -223,12 +228,14 @@
                                     new HashMap<>(),
                                     null);
             String content = StreamUtil.getStringFromStream(response.getContent());
-            String value = new JSONObject(content).getString("state");
-            return ClusterCommand.State.valueOf(value);
+            JSONObject jsonContent = new JSONObject(content);
+            String value = jsonContent.getString("state");
+            String cancelReason = jsonContent.optString("cancel_reason", "");
+            return new ClusterCommandStatus(ClusterCommand.State.valueOf(value), cancelReason);
         } catch (IOException | JSONException | IllegalArgumentException e) {
             CLog.w("Failed to get state of request %s command %s", requestId, commandId);
             CLog.e(e);
-            return ClusterCommand.State.UNKNOWN;
+            return new ClusterCommandStatus(ClusterCommand.State.UNKNOWN, "");
         }
     }
 
diff --git a/src/com/android/tradefed/cluster/ClusterCommandConfigBuilder.java b/src/com/android/tradefed/cluster/ClusterCommandConfigBuilder.java
index 6e3ffbb..e50699c 100644
--- a/src/com/android/tradefed/cluster/ClusterCommandConfigBuilder.java
+++ b/src/com/android/tradefed/cluster/ClusterCommandConfigBuilder.java
@@ -192,7 +192,9 @@
         envVars.put("TF_WORK_DIR", mWorkDir.getAbsolutePath());
         envVars.putAll(mTestEnvironment.getEnvVars());
         envVars.putAll(mTestContext.getEnvVars());
+
         for (String serial : mCommand.getTargetDeviceSerials()) {
+            serial = ClusterHostUtil.getLocalDeviceSerial(serial);
             IDeviceConfiguration device =
                     new DeviceConfigurationHolder(String.format("TF_DEVICE_%d", index++));
             device.getDeviceRequirements().setSerial(serial);
@@ -203,6 +205,11 @@
         }
         deviceConfigs.get(0).addSpecificConfig(new ClusterBuildProvider());
         config.setDeviceConfigList(deviceConfigs);
+        // Perform target preparation in parallel with an unlimited timeout
+        // TODO(b/166455187): Consider making parallel setup options configurable
+        config.injectOptionValue("parallel-setup", "true");
+        config.injectOptionValue("parallel-setup-timeout", "0");
+
         config.setTest(new ClusterCommandLauncher());
         config.setLogSaver(new ClusterLogSaver());
         // TODO(b/135636270): return log path to TFC instead of relying on a specific filename
diff --git a/src/com/android/tradefed/cluster/ClusterCommandEvent.java b/src/com/android/tradefed/cluster/ClusterCommandEvent.java
index 725e48e..46db15c 100644
--- a/src/com/android/tradefed/cluster/ClusterCommandEvent.java
+++ b/src/com/android/tradefed/cluster/ClusterCommandEvent.java
@@ -38,6 +38,7 @@
     public static final String DATA_KEY_PASSED_TEST_COUNT = "passed_test_count";
     public static final String DATA_KEY_FAILED_TEST_RUN_COUNT = "failed_test_run_count";
     public static final String DATA_KEY_LOST_DEVICE_DETECTED = "device_lost_detected";
+    public static final String DATA_KEY_SUBPROCESS_COMMAND_ERROR = "subprocess_command_error";
 
     // Maximum size of an individual data string value.
     public static final int MAX_DATA_STRING_SIZE = 4095;
diff --git a/src/com/android/tradefed/cluster/ClusterCommandLauncher.java b/src/com/android/tradefed/cluster/ClusterCommandLauncher.java
index 63e34a3..0992202 100644
--- a/src/com/android/tradefed/cluster/ClusterCommandLauncher.java
+++ b/src/com/android/tradefed/cluster/ClusterCommandLauncher.java
@@ -26,6 +26,7 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.testtype.IInvocationContextReceiver;
@@ -41,12 +42,14 @@
 import com.android.tradefed.util.StreamUtil;
 import com.android.tradefed.util.StringEscapeUtils;
 import com.android.tradefed.util.StringUtil;
+import com.android.tradefed.util.SubprocessEventHelper.InvocationFailedEventInfo;
 import com.android.tradefed.util.SubprocessTestResultsParser;
 import com.android.tradefed.util.SystemUtil;
 
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
@@ -133,32 +136,10 @@
     }
 
     @Override
-    public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
-        // Get an expanded TF_PATH value.
-        String tfPath = getEnvVar(TF_PATH, System.getProperty(TF_JAR_DIR));
-        if (tfPath == null) {
-            throw new RuntimeException("cannot find TF path!");
-        }
-
-        // Construct a Java class path based on TF_PATH value.
-        // This expects TF_PATH to be a colon(:) separated list of paths where each path
-        // points to a specific jar file or folder.
-        // (example: path/to/tradefed.jar:path/to/tradefed/folder:...)
-        final Set<String> jars = new LinkedHashSet<>();
-        for (final String path : tfPath.split(":")) {
-            final File jarFile = new File(path);
-            if (!jarFile.exists()) {
-                CLog.w("TF_PATH %s doesn't exist; ignoring", path);
-                continue;
-            }
-            if (jarFile.isFile()) {
-                jars.add(jarFile.getAbsolutePath());
-            } else {
-                jars.add(new File(path, "*").getAbsolutePath());
-            }
-        }
-
-        IRunUtil runUtil = getRunUtil();
+    public void run(TestInformation testInfo, ITestInvocationListener listener)
+            throws DeviceNotAvailableException {
+        // Prepare a IRunUtil instance for running subprocesses.
+        final IRunUtil runUtil = getRunUtil();
         runUtil.setWorkingDir(mRootDir);
         // clear the TF_GLOBAL_CONFIG env, so another tradefed will not reuse the global config file
         runUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
@@ -171,11 +152,86 @@
         logDir.mkdirs();
         File stdoutFile = new File(logDir, "stdout.txt");
         File stderrFile = new File(logDir, "stderr.txt");
-        FileIdleMonitor monitor = createFileMonitor(stdoutFile, stderrFile);
 
+        // Run setup scripts.
+        runSetupScripts(runUtil, stdoutFile, stderrFile);
+
+        FileIdleMonitor monitor = createFileMonitor(stdoutFile, stderrFile);
         SubprocessTestResultsParser subprocessEventParser = null;
         try (FileOutputStream stdout = new FileOutputStream(stdoutFile);
                 FileOutputStream stderr = new FileOutputStream(stderrFile)) {
+
+            String classpath = buildJavaClasspath();
+
+            // TODO(b/129111645): use proto reporting if a test suite supports it.
+            if (mUseSubprocessReporting) {
+                subprocessEventParser =
+                        createSubprocessTestResultsParser(listener, true, mInvocationContext);
+                final String port = Integer.toString(subprocessEventParser.getSocketServerPort());
+                // Create injection jar for subprocess result reporter, which is used
+                // for pre-R xTS. The created jar is put in front position of the class path to
+                // override class with the same name.
+                final SubprocessReportingHelper mHelper =
+                        new SubprocessReportingHelper(mCommandLine, classpath, testWorkDir, port);
+                final File subprocessReporterJar = mHelper.buildSubprocessReporterJar();
+                classpath =
+                        String.format("%s:%s", subprocessReporterJar.getAbsolutePath(), classpath);
+            }
+
+            List<String> javaCommandArgs = buildJavaCommandArgs(classpath, mCommandLine);
+            CLog.i("Running a command line: %s", mCommandLine);
+            CLog.i("args = %s", javaCommandArgs);
+            CLog.i("test working directory = %s", testWorkDir);
+
+            monitor.start();
+            runUtil.setWorkingDir(testWorkDir);
+            CommandResult result =
+                    runUtil.runTimedCmd(
+                            mConfiguration.getCommandOptions().getInvocationTimeout(),
+                            stdout,
+                            stderr,
+                            javaCommandArgs.toArray(new String[javaCommandArgs.size()]));
+            if (!result.getStatus().equals(CommandStatus.SUCCESS)) {
+                String error = null;
+                Throwable cause = null;
+                if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
+                    error =
+                            String.format(
+                                    "Command timed out after %sms",
+                                    mConfiguration.getCommandOptions().getInvocationTimeout());
+                } else {
+                    error =
+                            String.format(
+                                    "Command finished unsuccessfully: status=%s, exit_code=%s",
+                                    result.getStatus(), result.getExitCode());
+                    InvocationFailedEventInfo errorInfo =
+                            subprocessEventParser.getReportedInvocationFailedEventInfo();
+                    if (errorInfo != null) {
+                        cause = errorInfo.mCause;
+                    } else {
+                        cause = new Throwable(FileUtil.readStringFromFile(stderrFile));
+                    }
+                }
+                throw new SubprocessCommandException(error, cause);
+            }
+            CLog.i("Successfully ran a command");
+
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } finally {
+            monitor.stop();
+            if (subprocessEventParser != null) {
+                subprocessEventParser.joinReceiver(
+                        MAX_EVENT_RECEIVER_WAIT_TIME.toMillis(), /* wait for connection */ false);
+                StreamUtil.close(subprocessEventParser);
+            }
+        }
+    }
+
+    private void runSetupScripts(
+            final IRunUtil runUtil, final File stdoutFile, final File stderrFile) {
+        try (FileOutputStream stdout = new FileOutputStream(stdoutFile);
+                FileOutputStream stderr = new FileOutputStream(stderrFile)) {
             long timeout = mScriptTimeout;
             long startTime = System.currentTimeMillis();
             for (String script : mSetupScripts) {
@@ -203,74 +259,42 @@
                             String.format("Setup scripts failed to run in %sms", mScriptTimeout));
                 }
             }
-
-            String classpath = ArrayUtil.join(":", jars);
-            String commandLine = mCommandLine;
-            if (classpath.isEmpty()) {
-                throw new RuntimeException(
-                        String.format("cannot find any TF jars from %s!", tfPath));
-            }
-
-            if (mOriginalCommandLine != null && !mOriginalCommandLine.equals(commandLine)) {
-                // Make sure a wrapper XML of the original command is available because retries
-                // try to run original commands in Q+. If the original command was run with
-                // subprocess reporting, a recorded command would be one with .xml suffix.
-                new SubprocessConfigBuilder()
-                        .setWorkingDir(testWorkDir)
-                        .setOriginalConfig(
-                                QuotationAwareTokenizer.tokenizeLine(mOriginalCommandLine)[0])
-                        .build();
-            }
-            if (mUseSubprocessReporting) {
-                SubprocessReportingHelper mHelper = new SubprocessReportingHelper();
-                // Create standalone jar for subprocess result reporter, which is used
-                // for pre-O cts. The created jar is put in front position of the class path to
-                // override class with the same name.
-                classpath =
-                        String.format(
-                                "%s:%s",
-                                mHelper.createSubprocessReporterJar(mRootDir).getAbsolutePath(),
-                                classpath);
-                subprocessEventParser =
-                        createSubprocessTestResultsParser(listener, true, mInvocationContext);
-                String port = Integer.toString(subprocessEventParser.getSocketServerPort());
-                commandLine = mHelper.buildNewCommandConfig(commandLine, port, testWorkDir);
-            }
-
-            List<String> javaCommandArgs = buildJavaCommandArgs(classpath, commandLine);
-            CLog.i("Running a command line: %s", commandLine);
-            CLog.i("args = %s", javaCommandArgs);
-            CLog.i("test working directory = %s", testWorkDir);
-
-            monitor.start();
-            runUtil.setWorkingDir(testWorkDir);
-            CommandResult result =
-                    runUtil.runTimedCmd(
-                            mConfiguration.getCommandOptions().getInvocationTimeout(),
-                            stdout,
-                            stderr,
-                            javaCommandArgs.toArray(new String[javaCommandArgs.size()]));
-            if (!result.getStatus().equals(CommandStatus.SUCCESS)) {
-                String error = null;
-                if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
-                    error = "timeout";
-                } else {
-                    error = FileUtil.readStringFromFile(stderrFile);
-                }
-                throw new RuntimeException(String.format("Command failed to run: %s", error));
-            }
-            CLog.i("Successfully ran a command");
-
         } catch (IOException e) {
-            throw new RuntimeException(e);
-        } finally {
-            monitor.stop();
-            if (subprocessEventParser != null) {
-                subprocessEventParser.joinReceiver(
-                        MAX_EVENT_RECEIVER_WAIT_TIME.toMillis(), /* wait for connection */ false);
-                StreamUtil.close(subprocessEventParser);
+            throw new UncheckedIOException("Error running setup scripts", e);
+        }
+    }
+
+    private String buildJavaClasspath() {
+        // Get an expanded TF_PATH value.
+        final String tfPath = getEnvVar(TF_PATH, System.getProperty(TF_JAR_DIR));
+        if (tfPath == null) {
+            throw new RuntimeException("cannot find TF path!");
+        }
+
+        // Construct a Java class path based on TF_PATH value.
+        // This expects TF_PATH to be a colon(:) separated list of paths where each path
+        // points to a specific jar file or folder.
+        // (example: path/to/tradefed.jar:path/to/tradefed/folder:...)
+        // TODO(b/162473907): deprecate TF_PATH.
+        final Set<String> jars = new LinkedHashSet<>();
+        for (final String path : tfPath.split(":")) {
+            final File jarFile = new File(path);
+            if (!jarFile.exists()) {
+                CLog.w("TF_PATH %s doesn't exist; ignoring", path);
+                continue;
+            }
+            if (jarFile.isFile()) {
+                jars.add(jarFile.getAbsolutePath());
+            } else {
+                // Add a folder path to the classpath to handle class file directories.
+                jars.add(jarFile.getAbsolutePath() + "/");
+                jars.add(new File(path, "*").getAbsolutePath());
             }
         }
+        if (jars.isEmpty()) {
+            throw new RuntimeException(String.format("cannot find any TF jars from %s!", tfPath));
+        }
+        return String.join(":", jars);
     }
 
     /** Build a shell command line to invoke a TF process. */
diff --git a/src/com/android/tradefed/cluster/ClusterCommandScheduler.java b/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
index 9702419..075b3e8 100644
--- a/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
+++ b/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
@@ -145,6 +145,7 @@
         private String mSummary;
         private Set<String> processedSummaries = new HashSet<>();
         private String mError;
+        private String mSubprocessCommandError;
         private File mWorkDir;
         private InvocationStatus mInvocationStatus;
 
@@ -261,6 +262,10 @@
             super.invocationFailed(cause);
 
             mError = StreamUtil.getStackTrace(cause);
+            if (cause instanceof SubprocessCommandException && cause.getCause() != null) {
+                // The inner exception holds an exception stack trace from a subprocess.
+                mSubprocessCommandError = cause.getCause().getMessage();
+            }
         }
 
         /** {@inheritDoc} */
@@ -272,6 +277,9 @@
                     createEventBuilder()
                             .setType(ClusterCommandEvent.Type.InvocationEnded)
                             .setData(ClusterCommandEvent.DATA_KEY_ERROR, mError)
+                            .setData(
+                                    ClusterCommandEvent.DATA_KEY_SUBPROCESS_COMMAND_ERROR,
+                                    mSubprocessCommandError)
                             .build();
             getClusterClient().getCommandEventUploader().postEvent(event);
             getClusterClient().getCommandEventUploader().flush();
@@ -318,6 +326,9 @@
                             .setType(ClusterCommandEvent.Type.InvocationCompleted)
                             .setInvocationStatus(mInvocationStatus)
                             .setData(ClusterCommandEvent.DATA_KEY_ERROR, mError)
+                            .setData(
+                                    ClusterCommandEvent.DATA_KEY_SUBPROCESS_COMMAND_ERROR,
+                                    mSubprocessCommandError)
                             .setData(ClusterCommandEvent.DATA_KEY_SUMMARY, mSummary)
                             .setData(
                                     ClusterCommandEvent.DATA_KEY_FETCH_BUILD_TIME_MILLIS,
@@ -380,22 +391,22 @@
             @Override
             public void run() {
                 try {
-                    // check cluster command's status
+                    // Check cluster command's status.
                     if (getClusterOptions().checkCommandState()) {
-                        ClusterCommand.State status =
+                        ClusterCommandStatus commandStatus =
                                 getClusterClient()
-                                        .getCommandState(
+                                        .getCommandStatus(
                                                 mCommandTask.getRequestId(),
                                                 mCommandTask.getCommandId());
-                        if (ClusterCommand.State.CANCELED.equals(status)) {
-                            // TODO: retrieve cancel reason from TFC.
+                        if (ClusterCommand.State.CANCELED.equals(commandStatus.getState())) {
                             String cause =
                                     String.format(
                                             "The cluster client %s has marked command "
-                                                    + "(requestId=%s, commandId=%s) canceled",
+                                                    + "(requestId=%s, commandId=%s) canceled with reason: %s",
                                             getClusterClient().getClass().getSimpleName(),
                                             mCommandTask.getRequestId(),
-                                            mCommandTask.getCommandId());
+                                            mCommandTask.getCommandId(),
+                                            commandStatus.getCancelReason());
                             CLog.w("Stop invocation due to: %s", cause);
                             Optional.ofNullable(getInvocationContext())
                                     .map(IInvocationContext::getInvocationId)
@@ -497,7 +508,6 @@
             String runTarget =
                     ClusterHostUtil.getRunTarget(
                             device, runTargetFormat, getClusterOptions().getDeviceTag());
-            CLog.d("%s is available", runTarget);
             devices.put(runTarget, device);
         }
         return devices;
@@ -605,6 +615,9 @@
                         ClusterCommandEvent.createEventBuilder(commandTask)
                                 .setHostName(ClusterHostUtil.getHostName())
                                 .setType(ClusterCommandEvent.Type.AllocationFailed)
+                                .setData(
+                                        ClusterCommandEvent.DATA_KEY_ERROR,
+                                        StreamUtil.getStackTrace(e))
                                 .build());
                 eventUploader.flush();
             } catch (ConfigurationException | IOException | JSONException e) {
@@ -616,7 +629,9 @@
                         ClusterCommandEvent.createEventBuilder(commandTask)
                                 .setHostName(ClusterHostUtil.getHostName())
                                 .setType(ClusterCommandEvent.Type.ConfigurationError)
-                                .setData(ClusterCommandEvent.DATA_KEY_ERROR, e.toString())
+                                .setData(
+                                        ClusterCommandEvent.DATA_KEY_ERROR,
+                                        StreamUtil.getStackTrace(e))
                                 .build());
                 eventUploader.flush();
             }
@@ -638,7 +653,7 @@
         if (commandTask.getTargetDeviceSerials() != null) {
             for (String serial : commandTask.getTargetDeviceSerials()) {
                 cmdLine += " --serial ";
-                cmdLine += serial;
+                cmdLine += ClusterHostUtil.getLocalDeviceSerial(serial);
             }
         }
         CLog.i("executing cluster command: [%s] %s", commandTask.getTaskId(), cmdLine);
@@ -692,8 +707,7 @@
      */
     protected boolean dryRunCommand(final InvocationEventHandler handler, String[] args)
             throws ConfigurationException {
-        IConfiguration config =
-                getConfigFactory().createConfigurationFromArgs(args, null, getKeyStoreClient());
+        IConfiguration config = createConfiguration(args);
         if (config.getCommandOptions().isDryRunMode()) {
             IInvocationContext context = new InvocationContext();
             context.addDeviceBuildInfo("stub", new BuildInfo());
diff --git a/src/com/android/tradefed/cluster/ClusterCommandStatus.java b/src/com/android/tradefed/cluster/ClusterCommandStatus.java
new file mode 100644
index 0000000..ef95614
--- /dev/null
+++ b/src/com/android/tradefed/cluster/ClusterCommandStatus.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.cluster;
+
+/** A class that represents the state and the cancel reason for a command from TF Cluster. */
+public class ClusterCommandStatus {
+    private final ClusterCommand.State mState;
+    private final String mCancelReason;
+
+    /**
+     * Construct
+     *
+     * @param state state of the command.
+     * @param cancelReason cancel reason of the command (if canceled).
+     */
+    public ClusterCommandStatus(ClusterCommand.State state, String cancelReason) {
+        mState = state;
+        mCancelReason = cancelReason;
+    }
+
+    public ClusterCommand.State getState() {
+        return mState;
+    }
+
+    public String getCancelReason() {
+        return mCancelReason;
+    }
+}
diff --git a/src/com/android/tradefed/cluster/ClusterDeviceInfo.java b/src/com/android/tradefed/cluster/ClusterDeviceInfo.java
index 23c39eb..0fcf346 100644
--- a/src/com/android/tradefed/cluster/ClusterDeviceInfo.java
+++ b/src/com/android/tradefed/cluster/ClusterDeviceInfo.java
@@ -82,7 +82,7 @@
      */
     public JSONObject toJSON() throws JSONException {
         final JSONObject json = new JSONObject();
-        json.put("device_serial", mDeviceDescriptor.getSerial());
+        json.put("device_serial", ClusterHostUtil.getUniqueDeviceSerial(mDeviceDescriptor));
         json.put("run_target", mRunTarget);
         json.put("build_id", mDeviceDescriptor.getBuildId());
         json.put("product", mDeviceDescriptor.getProduct());
diff --git a/src/com/android/tradefed/cluster/ClusterDeviceMonitor.java b/src/com/android/tradefed/cluster/ClusterDeviceMonitor.java
index 0ca1be1..c8f0fa5 100644
--- a/src/com/android/tradefed/cluster/ClusterDeviceMonitor.java
+++ b/src/com/android/tradefed/cluster/ClusterDeviceMonitor.java
@@ -108,6 +108,12 @@
                             .setHostName(ClusterHostUtil.getHostName())
                             .setTfVersion(ClusterHostUtil.getTfVersion())
                             .setData(getAdditionalHostInfo())
+                            .setData(
+                                    ClusterHostEvent.HOST_IP_KEY,
+                                    ClusterHostUtil.getHostIpAddress())
+                            .setData(
+                                    ClusterHostEvent.LABEL_KEY,
+                                    String.join(",", getClusterOptions().getLabels()))
                             .setClusterId(getClusterOptions().getClusterId())
                             .setNextClusterIds(getClusterOptions().getNextClusterIds())
                             .setLabName(getClusterOptions().getLabName());
diff --git a/src/com/android/tradefed/cluster/ClusterHostEvent.java b/src/com/android/tradefed/cluster/ClusterHostEvent.java
index 8a56e93..b3bb430 100644
--- a/src/com/android/tradefed/cluster/ClusterHostEvent.java
+++ b/src/com/android/tradefed/cluster/ClusterHostEvent.java
@@ -37,6 +37,8 @@
     private Map<String, String> mData = new HashMap<>();
     private String mLabName;
     public static final String EVENT_QUEUE = "host-event-queue";
+    public static final String LABEL_KEY = "label";
+    public static final String HOST_IP_KEY = "host_ip";
 
     /** Enums of the different types of host events. */
     public enum HostEventType {
diff --git a/src/com/android/tradefed/cluster/ClusterHostUtil.java b/src/com/android/tradefed/cluster/ClusterHostUtil.java
index b68c9c1..5bc6209 100644
--- a/src/com/android/tradefed/cluster/ClusterHostUtil.java
+++ b/src/com/android/tradefed/cluster/ClusterHostUtil.java
@@ -27,36 +27,137 @@
 import com.google.common.net.InetAddresses;
 import com.google.common.primitives.Longs;
 
+import java.net.Inet6Address;
 import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
 import java.net.UnknownHostException;
 import java.security.InvalidParameterException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+
 /** Static util functions for TF Cluster to get global config instances, host information, etc. */
 public class ClusterHostUtil {
 
     private static String sHostName = null;
 
+    private static String sHostIpAddress = null;
+
     static final String DEFAULT_TF_VERSION = "(unknown)";
+    static final String EMULATOR_SERIAL_PREFIX = "emulator-";
+    static final String NULL_DEVICE_SERIAL_PLACEHOLDER = "(no device serial)";
+    static final String UNKNOWN = "UNKNOWN";
 
     private static long sTfStartTime = getCurrentTimeMillis();
 
     /**
      * Gets the hostname.
      *
-     * @return the hostname or null if we were unable to fetch it
+     * <p>1. Try to get hostname from InetAddress. 2. If fail, try to get hostname from HOSTNAME
+     * env. 3. If not set, generate a unique hostname.
+     *
+     * @return the hostname or null if we were unable to fetch it.
      */
     public static String getHostName() {
-        if (sHostName == null) {
+        if (sHostName != null) {
+            return sHostName;
+        }
+        try {
+            sHostName = InetAddress.getLocalHost().getHostName();
+            return sHostName;
+        } catch (UnknownHostException e) {
+            CLog.w("Failed to get hostname from InetAddress: %s", e);
+        }
+        CLog.i("Get hostname from HOSTNAME env.");
+        sHostName = System.getenv("HOSTNAME");
+        if (!Strings.isNullOrEmpty(sHostName)) {
+            return sHostName;
+        }
+        sHostName = "unknown-" + UUID.randomUUID().toString();
+        CLog.i("No HOSTNAME env set. Generate hostname: %s.", sHostName);
+        return sHostName;
+    }
+
+    /**
+     * Returns a unique device serial for a device.
+     *
+     * <p>Non-physical devices (e.g. emulator) have pseudo serials which are not unique across
+     * hosts. This method prefixes those with a hostname to make them unique.
+     *
+     * @param device a device descriptor.
+     * @return a unique device serial.
+     */
+    public static String getUniqueDeviceSerial(DeviceDescriptor device) {
+        String serial = device.getSerial();
+        if (Strings.isNullOrEmpty(serial)
+                || device.isStubDevice()
+                || serial.startsWith(EMULATOR_SERIAL_PREFIX)) {
+            if (Strings.isNullOrEmpty(serial)) {
+                serial = NULL_DEVICE_SERIAL_PLACEHOLDER;
+            }
+            serial = String.format("%s:%s", getHostName(), serial);
+        }
+        return serial;
+    }
+
+    /**
+     * Returns a local device serial for a given unique device serial.
+     *
+     * <p>TFC sends down unique device serials for non-physical devices which TF does not
+     * understand. This method converts them back to local device serials.
+     *
+     * @param serial a unique device serial from TFC.
+     * @return a local device serial.
+     */
+    public static String getLocalDeviceSerial(String serial) {
+        String prefix = getHostName() + ":";
+        if (serial.startsWith(prefix)) {
+            return serial.substring(prefix.length());
+        }
+        return serial;
+    }
+    /**
+     * Gets the IP address.
+     *
+     * @return the IPV4 address String or "UNKNOWN" if we were unable to fetch it.
+     */
+    public static String getHostIpAddress() {
+        if (sHostIpAddress == null) {
+            List<InetAddress> addresses = new ArrayList<>();
             try {
-                sHostName = InetAddress.getLocalHost().getHostName();
-            } catch (UnknownHostException e) {
-                CLog.w("failed to get hostname: %s", e);
+                Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+                if (interfaces == null) {
+                    return UNKNOWN;
+                }
+                for (NetworkInterface networkInterface : Collections.list(interfaces)) {
+                    if (!networkInterface.isUp() || networkInterface.isLoopback()) {
+                        continue;
+                    }
+                    for (InetAddress address :
+                            Collections.list(networkInterface.getInetAddresses())) {
+                        if (address.isLinkLocalAddress()
+                                || address.isLoopbackAddress()
+                                || address instanceof Inet6Address) {
+                            continue;
+                        }
+                        addresses.add(address);
+                    }
+                }
+            } catch (SocketException e) {
+                CLog.w(e);
+            }
+            if (!addresses.isEmpty()) {
+                sHostIpAddress = addresses.get(0).getHostAddress();
             }
         }
-        return sHostName;
+        return sHostIpAddress == null ? UNKNOWN : sHostIpAddress;
     }
 
     /**
@@ -125,7 +226,7 @@
                         txt = device.getDeviceClass();
                         break;
                     case "SERIAL":
-                        txt = device.getSerial();
+                        txt = getUniqueDeviceSerial(device);
                         break;
                     case "TAG":
                         if (deviceTags == null || deviceTags.isEmpty()) {
diff --git a/src/com/android/tradefed/cluster/IClusterClient.java b/src/com/android/tradefed/cluster/IClusterClient.java
index e803d7a..d232af0 100644
--- a/src/com/android/tradefed/cluster/IClusterClient.java
+++ b/src/com/android/tradefed/cluster/IClusterClient.java
@@ -90,6 +90,20 @@
             throws IOException, JSONException;
 
     /**
+     * Get the command status of a cluster command (the state and the cancel reason if canceled).
+     *
+     * @param requestId cluster request ID
+     * @param commandId cluster command ID
+     * @return a ClusterCommandStatus that represents the state and the cancel reason if the command
+     *     is canceled. The state is {@link ClusterCommand.State#UNKNOWN} if it could not be
+     *     determined.
+     */
+    public default ClusterCommandStatus getCommandStatus(String requestId, String commandId) {
+        ClusterCommand.State state = getCommandState(requestId, commandId);
+        return new ClusterCommandStatus(state, "");
+    }
+
+    /**
      * Determine the state of a cluster command.
      *
      * @param requestId cluster request ID
diff --git a/src/com/android/tradefed/cluster/SubprocessCommandException.java b/src/com/android/tradefed/cluster/SubprocessCommandException.java
new file mode 100644
index 0000000..3be5dc9
--- /dev/null
+++ b/src/com/android/tradefed/cluster/SubprocessCommandException.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.cluster;
+
+/** A subprocess command failed to run. */
+public class SubprocessCommandException extends RuntimeException {
+
+    public SubprocessCommandException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/src/com/android/tradefed/cluster/SubprocessConfigBuilder.java b/src/com/android/tradefed/cluster/SubprocessConfigBuilder.java
index 155b80b..6952542 100644
--- a/src/com/android/tradefed/cluster/SubprocessConfigBuilder.java
+++ b/src/com/android/tradefed/cluster/SubprocessConfigBuilder.java
@@ -18,12 +18,32 @@
 import com.android.tradefed.config.Configuration;
 import com.android.tradefed.config.ConfigurationUtil;
 import com.android.tradefed.result.LegacySubprocessResultsReporter;
+import com.android.tradefed.util.FileUtil;
 
-import org.kxml2.io.KXmlSerializer;
+import com.google.common.base.Strings;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.xml.sax.SAXException;
 
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.PrintWriter;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
 
 /**
  * Build a wrapper TF config XML for an existing TF config.
@@ -31,19 +51,23 @@
  * <p>A wrapper XML allows to enable subprocess reporting on an existing TF config.
  */
 public class SubprocessConfigBuilder {
-    private static final String INCLUDE_NAME = "include";
     private static final String REPORTER_CLASS = LegacySubprocessResultsReporter.class.getName();
     private static final String OPTION_KEY = "subprocess-report-port";
-    private static final String CONFIG_DESCRIPTION = "Cluster Command Launcher config";
+    private String mClasspath;
 
-    private File mWorkdir;
+    private File mWorkDir;
 
     private String mOriginalConfig;
 
     private String mPort;
 
+    public SubprocessConfigBuilder setClasspath(String classpath) {
+        mClasspath = classpath;
+        return this;
+    }
+
     public SubprocessConfigBuilder setWorkingDir(File dir) {
-        mWorkdir = dir;
+        mWorkDir = dir;
         return this;
     }
 
@@ -66,43 +90,79 @@
     }
 
     public File build() throws IOException {
-        // Make a new config name based on the original config name to make it possible to find
-        // out the original command line from a modified one.
-        // FIXME: Find a better way to preserve the original command line.
-        String configName = createConfigName(mOriginalConfig);
-        // mOriginalConfig is from another test suite, so its content is hard to know at this
-        // time. So it doesn't load mOriginalConfig as IConfiguration and add additional config.
-        // Instead, it creates a wrapper config including mOriginalConfig.
-        File f = new File(mWorkdir, configName);
-        PrintWriter writer = new PrintWriter(f);
-        KXmlSerializer serializer = new KXmlSerializer();
-        serializer.setOutput(writer);
-        serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
-        serializer.startDocument("UTF-8", null);
-        serializer.startTag(null, ConfigurationUtil.CONFIGURATION_NAME);
-        serializer.attribute(
-                null, Configuration.CONFIGURATION_DESCRIPTION_TYPE_NAME, CONFIG_DESCRIPTION);
-
-        serializer.startTag(null, INCLUDE_NAME);
-        serializer.attribute(null, ConfigurationUtil.NAME_NAME, mOriginalConfig);
-        serializer.endTag(null, INCLUDE_NAME);
-
-        if (mPort != null) {
-            serializer.startTag(null, Configuration.RESULT_REPORTER_TYPE_NAME);
-            serializer.attribute(null, ConfigurationUtil.CLASS_NAME, REPORTER_CLASS);
-
-            serializer.startTag(null, ConfigurationUtil.OPTION_NAME);
-            serializer.attribute(null, ConfigurationUtil.NAME_NAME, OPTION_KEY);
-            serializer.attribute(null, ConfigurationUtil.VALUE_NAME, mPort);
-            serializer.endTag(null, ConfigurationUtil.OPTION_NAME);
-
-            serializer.endTag(null, Configuration.RESULT_REPORTER_TYPE_NAME);
+        final List<URL> urls = new ArrayList<>();
+        for (final String path : mClasspath.split(File.pathSeparator)) {
+            if (path.endsWith("*")) {
+                final File dir = new File(path.substring(0, path.length() - 1));
+                if (!dir.exists()) {
+                    continue;
+                }
+                for (final File file :
+                        dir.listFiles((parent, name) -> name.toLowerCase().endsWith(".jar"))) {
+                    urls.add(file.toURI().toURL());
+                }
+            } else {
+                urls.add(new File(path).toURI().toURL());
+            }
         }
 
-        serializer.endTag(null, ConfigurationUtil.CONFIGURATION_NAME);
-        serializer.endDocument();
+        // Read the original config file.
+        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        Document doc = null;
+        try (URLClassLoader loader = new URLClassLoader(urls.toArray(new URL[urls.size()]), null)) {
+            final DocumentBuilder builder = factory.newDocumentBuilder();
+            final String ext = FileUtil.getExtension(mOriginalConfig);
+            InputStream in = null;
+            if (Strings.isNullOrEmpty(ext)) {
+                in = loader.getResourceAsStream(String.format("config/%s.xml", mOriginalConfig));
+            } else {
+                in = loader.getResourceAsStream(String.format("config/%s", mOriginalConfig));
+            }
+            if (in == null) {
+                File f = new File(mOriginalConfig);
+                if (!f.isAbsolute()) {
+                    f = new File(mWorkDir, mOriginalConfig);
+                }
+                try {
+                    in = new FileInputStream(f);
+                } catch (FileNotFoundException e) {
+                    throw new RuntimeException(
+                            String.format("Could not find configuration '%s'", mOriginalConfig));
+                }
+            }
+            doc = builder.parse(in);
+        } catch (ParserConfigurationException | SAXException e) {
+            throw new RuntimeException(e);
+        }
 
-        writer.close();
+        if (mPort != null) {
+            // Add subprocess result reporter to a config file.
+            final Node root = doc.getElementsByTagName("configuration").item(0);
+            final Element reporter = doc.createElement(Configuration.RESULT_REPORTER_TYPE_NAME);
+            reporter.setAttribute(ConfigurationUtil.CLASS_NAME, REPORTER_CLASS);
+            final Element options = doc.createElement(ConfigurationUtil.OPTION_NAME);
+            options.setAttribute(ConfigurationUtil.NAME_NAME, OPTION_KEY);
+            options.setAttribute(ConfigurationUtil.VALUE_NAME, mPort);
+            reporter.appendChild(options);
+            root.appendChild(reporter);
+        }
+
+        File f = new File(mWorkDir, mOriginalConfig);
+        if (!f.exists() || !f.isFile()) {
+            // If the original config is an existing file, we need to update it since some old TFs
+            // check the file system first before bundled configs when loading configs.
+            // If the original config is not an existing file, we can use any name since the
+            // original config name will be assigned when creating a injection jar.
+            f = File.createTempFile("subprocess_config_", ".xml", mWorkDir);
+        }
+        TransformerFactory transformerFactory = TransformerFactory.newInstance();
+        try {
+            Transformer transformer = transformerFactory.newTransformer();
+            transformer.transform(new DOMSource(doc), new StreamResult(f));
+        } catch (TransformerException e) {
+            throw new RuntimeException(e);
+        }
+
         return f;
     }
 }
diff --git a/src/com/android/tradefed/cluster/SubprocessReportingHelper.java b/src/com/android/tradefed/cluster/SubprocessReportingHelper.java
index d48522e..4944859 100644
--- a/src/com/android/tradefed/cluster/SubprocessReportingHelper.java
+++ b/src/com/android/tradefed/cluster/SubprocessReportingHelper.java
@@ -22,6 +22,8 @@
 import com.android.tradefed.util.StreamUtil;
 import com.android.tradefed.util.ZipUtil2;
 
+import com.google.common.base.Strings;
+
 import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileInputStream;
@@ -40,44 +42,71 @@
     private static final String REPORTER_JAR_NAME = "subprocess-results-reporter.jar";
     private static final String CLASS_FILTER =
             String.format(
-                    "(^%s|^%s|^%s|^%s|^%s).*class$",
+                    "(^%s|^%s|^%s|^%s|^%s|^%s).*class$",
+                    "ErrorIdentifier",
                     "LegacySubprocessResultsReporter",
                     "SubprocessTestResultsParser",
                     "SubprocessEventHelper",
                     "SubprocessResultsReporter",
                     "ISupportGranularResults");
 
+    private String mCommandLine;
+    private String mClasspath;
+    private File mWorkDir;
+    private String mPort;
+
+    public SubprocessReportingHelper(
+            String commandLine, String classpath, File workDir, String port) {
+        mCommandLine = commandLine;
+        mClasspath = classpath;
+        mWorkDir = workDir;
+        mPort = port;
+    }
+
     /**
      * Dynamically generate extract .class file from tradefed.jar and generate new subprocess
      * results reporter jar.
      *
-     * @param parentDir parent directory of subprocess results reporter jar.
-     * @return subprocess result reporter jar.
+     * @return a subprocess result reporter jar to inject.
      * @throws IOException
      */
-    public File createSubprocessReporterJar(File parentDir) throws IOException {
-        File reporterJar = new File(parentDir, REPORTER_JAR_NAME);
-        File tfJar =
+    public File buildSubprocessReporterJar() throws IOException {
+        // Generate a patched config file.
+        final String[] tokens = QuotationAwareTokenizer.tokenizeLine(mCommandLine);
+        final String configName = tokens[0];
+        final SubprocessConfigBuilder builder = new SubprocessConfigBuilder();
+        builder.setWorkingDir(mWorkDir)
+                .setOriginalConfig(configName)
+                .setClasspath(mClasspath)
+                .setPort(mPort);
+        final File patchedConfigFile = builder.build();
+        LogUtil.CLog.i(
+                "Generating new configuration:\n %s",
+                FileUtil.readStringFromFile(patchedConfigFile));
+
+        final File reporterJar = new File(mWorkDir, REPORTER_JAR_NAME);
+        final File tfJar =
                 new File(
                         LegacySubprocessResultsReporter.class
                                 .getProtectionDomain()
                                 .getCodeSource()
                                 .getLocation()
                                 .getPath());
+        final String ext = FileUtil.getExtension(configName);
+        final String configFileName = Strings.isNullOrEmpty(ext) ? configName + ".xml" : configName;
         // tfJar is directory of .class file when running JUnit test from Eclipse IDE
         if (tfJar.isDirectory()) {
             Set<File> classFiles = FileUtil.findFilesObject(tfJar, CLASS_FILTER);
             Manifest manifest = new Manifest();
-            createJar(reporterJar, manifest, classFiles);
-        }
-        // tfJar is the tradefed.jar when running with tradefed.
-        else {
+            createJar(reporterJar, manifest, classFiles, configFileName, patchedConfigFile);
+        } else {
+            // tfJar is the tradefed.jar when running with tradefed.
             File extractedJar = ZipUtil2.extractZipToTemp(tfJar, "tmp-jar");
             try {
                 Set<File> classFiles = FileUtil.findFilesObject(extractedJar, CLASS_FILTER);
                 File mf = FileUtil.findFile(extractedJar, "MANIFEST.MF");
                 Manifest manifest = new Manifest(new FileInputStream(mf));
-                createJar(reporterJar, manifest, classFiles);
+                createJar(reporterJar, manifest, classFiles, configFileName, patchedConfigFile);
             } finally {
                 FileUtil.recursiveDelete(extractedJar);
             }
@@ -92,7 +121,9 @@
      * @param manifest manifest file.
      * @throws IOException
      */
-    private void createJar(File jar, Manifest manifest, Set<File> classFiles) throws IOException {
+    private void createJar(
+            File jar, Manifest manifest, Set<File> classFiles, String configName, File configFile)
+            throws IOException {
         try (JarOutputStream jarOutput = new JarOutputStream(new FileOutputStream(jar), manifest)) {
             for (File file : classFiles) {
                 try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file))) {
@@ -104,30 +135,14 @@
                     jarOutput.closeEntry();
                 }
             }
+            try (BufferedInputStream in =
+                    new BufferedInputStream(new FileInputStream(configFile))) {
+                JarEntry entry = new JarEntry(String.format("config/%s", configName));
+                entry.setTime(configFile.lastModified());
+                jarOutput.putNextEntry(entry);
+                StreamUtil.copyStreams(in, jarOutput);
+                jarOutput.closeEntry();
+            }
         }
     }
-
-    /**
-     * Get a new command line whose configuration argument is replaced by a newly-created wrapper
-     * configuration.
-     *
-     * <p>The resulting command line will reference a generate XML file in parentDir and needs to
-     * run from parentDir.
-     *
-     * @param commandLine old command line that will be run by subprocess.
-     * @param port port number that subprocess should use to report results.
-     * @param parentDir parent directory of new wrapper configuration.
-     * @return new command line, whose first argument is wrapper config.
-     * @throws IOException
-     */
-    public String buildNewCommandConfig(String commandLine, String port, File parentDir)
-            throws IOException {
-        String[] tokens = QuotationAwareTokenizer.tokenizeLine(commandLine);
-        SubprocessConfigBuilder builder = new SubprocessConfigBuilder();
-        builder.setWorkingDir(parentDir).setOriginalConfig(tokens[0]).setPort(port);
-        File f = builder.build();
-        LogUtil.CLog.i("Generating new configuration:\n %s", FileUtil.readStringFromFile(f));
-        tokens[0] = f.getName();
-        return QuotationAwareTokenizer.combineTokens(tokens);
-    }
 }
diff --git a/src/com/android/tradefed/command/CommandOptions.java b/src/com/android/tradefed/command/CommandOptions.java
index 1760e9c..b3c5ed3 100644
--- a/src/com/android/tradefed/command/CommandOptions.java
+++ b/src/com/android/tradefed/command/CommandOptions.java
@@ -23,6 +23,7 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.UniqueMultiMap;
 
+import java.time.Duration;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.Map;
@@ -108,6 +109,10 @@
             "[0, shard-count)")
     private Integer mShardIndex;
 
+    @Option(name = "optimize-mainline-test", description =
+            "Whether or not to optimize the list of test modules for mainline.")
+    private boolean mOptimizeMainlineTest;
+
     @Option(
         name = "enable-token-sharding",
         description = "Whether or not to allow sharding with the token support enabled."
@@ -161,6 +166,12 @@
                     "For remote sharded invocation, whether or not to attempt the setup in parallel.")
     private boolean mUseParallelRemoteSetup = false;
 
+    @Option(name = "parallel-setup", description = "Whether to attempt the setup in parallel.")
+    private boolean mUseParallelSetup = false;
+
+    @Option(name = "parallel-setup-timeout", description = "Timeout to use during parallel setup.")
+    private Duration mParallelSetupTimeout = Duration.ofMinutes(30L);
+
     @Option(
             name = "replicate-parent-setup",
             description =
@@ -370,6 +381,14 @@
      * {@inheritDoc}
      */
     @Override
+    public boolean getOptimizeMainlineTest() {
+        return mOptimizeMainlineTest;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public Integer getShardCount() {
         return mShardCount;
     }
@@ -514,6 +533,18 @@
 
     /** {@inheritDoc} */
     @Override
+    public boolean shouldUseParallelSetup() {
+        return mUseParallelSetup;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Duration getParallelSetupTimeout() {
+        return mParallelSetupTimeout;
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public boolean shouldUseReplicateSetup() {
         return mReplicateParentSetup;
     }
diff --git a/src/com/android/tradefed/command/CommandRunner.java b/src/com/android/tradefed/command/CommandRunner.java
index fe77903..0cb94fe 100644
--- a/src/com/android/tradefed/command/CommandRunner.java
+++ b/src/com/android/tradefed/command/CommandRunner.java
@@ -21,6 +21,7 @@
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.device.NoDeviceException;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.SerializationUtil;
 
@@ -119,7 +120,10 @@
             // After 1 min we check if the command was executed.
             if (mScheduler.getReadyCommandCount() > 0
                     && mScheduler.getExecutingCommandCount() == 0) {
-                printStackTrace(new NoDeviceException("No device was allocated for the command."));
+                printStackTrace(
+                        new NoDeviceException(
+                                "No device was allocated for the command.",
+                                InfraErrorIdentifier.RUNNER_ALLOCATION_ERROR));
                 mErrorCode = ExitCode.NO_DEVICE_ALLOCATED;
                 mScheduler.removeAllCommands();
                 mScheduler.shutdown();
diff --git a/src/com/android/tradefed/command/CommandScheduler.java b/src/com/android/tradefed/command/CommandScheduler.java
index 708b0d2..0ee2ca0 100644
--- a/src/com/android/tradefed/command/CommandScheduler.java
+++ b/src/com/android/tradefed/command/CommandScheduler.java
@@ -19,6 +19,7 @@
 import com.android.ddmlib.DdmPreferences;
 import com.android.ddmlib.Log;
 import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.build.BuildRetrievalError;
 import com.android.tradefed.clearcut.ClearcutClient;
 import com.android.tradefed.command.CommandFileParser.CommandLine;
 import com.android.tradefed.command.CommandFileWatcher.ICommandFileListener;
@@ -33,6 +34,7 @@
 import com.android.tradefed.config.ConfigurationDescriptor;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.ConfigurationFactory;
+import com.android.tradefed.config.DynamicRemoteFileResolver;
 import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.IConfigurationFactory;
@@ -67,6 +69,7 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.ResultForwarder;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.result.suite.SuiteResultReporter;
 import com.android.tradefed.sandbox.ISandbox;
 import com.android.tradefed.testtype.IRemoteTest;
@@ -1073,8 +1076,9 @@
                 IConfiguration config = cmd.getConfiguration();
                 IInvocationContext context = new InvocationContext();
                 context.setConfigurationDescriptor(config.getConfigurationDescription());
-                Map<String, ITestDevice> devices = allocateDevices(config, manager);
-                if (!devices.isEmpty()) {
+                DeviceAllocationResult allocationResults = allocateDevices(config, manager);
+                if (allocationResults.wasAllocationSuccessful()) {
+                    Map<String, ITestDevice> devices = allocationResults.getAllocatedDevices();
                     cmdIter.remove();
                     mExecutingCommands.add(cmd);
                     context.addAllocatedDevice(devices);
@@ -1183,12 +1187,27 @@
         return foundSandbox;
     }
 
-    private boolean isProxyCommand(IConfiguration config) {
-        return config.getConfigurationObject(ProxyConfiguration.PROXY_CONFIG_TYPE_KEY) != null;
+    private boolean isProxyCommand(String[] args) throws ConfigurationException {
+        ProxyConfiguration proxy = new ProxyConfiguration();
+        ArgsOptionParser argsParser = new ArgsOptionParser(proxy);
+        List<String> argsList = new ArrayList<>(Arrays.asList(args));
+        argsList.remove(0);
+        argsParser.parseBestEffort(argsList, true);
+        return proxy.isProxySet();
     }
 
-    private String[] handleProxyCommand(IConfiguration config, String[] originalArgs)
-            throws ConfigurationException {
+    private IConfiguration handleProxyCommand(String[] originalArgs) throws ConfigurationException {
+        IConfiguration config =
+                ((ConfigurationFactory) getConfigFactory())
+                        .createPartialConfigurationFromArgs(
+                                originalArgs,
+                                getKeyStoreClient(),
+                                ImmutableSet.of(ProxyConfiguration.PROXY_CONFIG_TYPE_KEY));
+        try {
+            config.resolveDynamicOptions(new DynamicRemoteFileResolver());
+        } catch (BuildRetrievalError e) {
+            throw new ConfigurationException(e.getMessage(), e);
+        }
         ProxyConfiguration proxy =
                 (ProxyConfiguration)
                         config.getConfigurationObject(ProxyConfiguration.PROXY_CONFIG_TYPE_KEY);
@@ -1196,7 +1215,7 @@
             throw new ConfigurationException("No proxy configuration found.");
         }
         originalArgs[0] = proxy.getProxyConfig().getAbsolutePath();
-        return originalArgs;
+        return config;
     }
 
     /** Returns true if the configuration used is a retry one. */
@@ -1217,31 +1236,28 @@
         return GlobalConfiguration.getInstance().getSandboxFactory().createSandbox();
     }
 
-    private IConfiguration createConfiguration(String[] args) throws ConfigurationException {
-        TradefedDelegator delegator = new TradefedDelegator();
-        ArgsOptionParser argsParser = new ArgsOptionParser(delegator);
-        List<String> argsList = new ArrayList<>(Arrays.asList(args));
-        argsList.remove(0);
-        argsParser.parseBestEffort(argsList, true);
+    protected IConfiguration createConfiguration(String[] args) throws ConfigurationException {
+        TradefedDelegator delegator = checkDelegation(args);
         if (delegator.shouldUseDelegation()) {
-            String[] argsWithoutDelegation = TradefedDelegator.clearCommandline(args);
-            delegator.setCommandLine(argsWithoutDelegation);
-            CLog.d(
-                    "Using commandline arguments as starting command: %s",
-                    Arrays.asList(argsWithoutDelegation));
-            IConfiguration config =
-                    ((ConfigurationFactory) getConfigFactory())
-                            .createPartialConfigurationFromArgs(
-                                    argsWithoutDelegation,
-                                    getKeyStoreClient(),
-                                    ImmutableSet.of(
-                                            Configuration.DEVICE_REQUIREMENTS_TYPE_NAME,
-                                            Configuration.LOGGER_TYPE_NAME,
-                                            Configuration.LOG_SAVER_TYPE_NAME,
-                                            Configuration.RESULT_REPORTER_TYPE_NAME));
-            config.setConfigurationObject(TradefedDelegator.DELEGATE_OBJECT, delegator);
-            setDelegateLevelReporting(config);
-            return config;
+            args = TradefedDelegator.clearCommandline(args);
+            // Do not use delegation on staging
+            if (!delegator.isStaging()) {
+                delegator.setCommandLine(args);
+                CLog.d("Using commandline arguments as starting command: %s", Arrays.asList(args));
+                IConfiguration config =
+                        ((ConfigurationFactory) getConfigFactory())
+                                .createPartialConfigurationFromArgs(
+                                        args,
+                                        getKeyStoreClient(),
+                                        ImmutableSet.of(
+                                                Configuration.DEVICE_REQUIREMENTS_TYPE_NAME,
+                                                Configuration.LOGGER_TYPE_NAME,
+                                                Configuration.LOG_SAVER_TYPE_NAME,
+                                                Configuration.RESULT_REPORTER_TYPE_NAME));
+                config.setConfigurationObject(TradefedDelegator.DELEGATE_OBJECT, delegator);
+                setDelegateLevelReporting(config);
+                return config;
+            }
         }
 
         // check if the command should be sandboxed
@@ -1251,22 +1267,44 @@
             return SandboxConfigurationFactory.getInstance()
                     .createConfigurationFromArgs(args, getKeyStoreClient(), sandbox, new RunUtil());
         }
+        if (isProxyCommand(args)) {
+            IConfiguration proxyConfig = handleProxyCommand(args);
+            String[] argsWithoutDelegation = ProxyConfiguration.clearCommandline(args);
+            IConfiguration resolvedConfig = null;
+            try {
+                resolvedConfig =
+                        getConfigFactory()
+                                .createConfigurationFromArgs(
+                                        argsWithoutDelegation, null, getKeyStoreClient());
+            } catch (ConfigurationException e) {
+                proxyConfig.cleanConfigurationData();
+                throw e;
+            }
+            resolvedConfig.addFilesToClean(proxyConfig.getFilesToClean());
+            return resolvedConfig;
+        }
         IConfiguration config =
                 getConfigFactory().createConfigurationFromArgs(args, null, getKeyStoreClient());
-        if (isProxyCommand(config)) {
-            String[] newArgs = handleProxyCommand(config, args);
-            IConfiguration proxyConfig =
-                    getConfigFactory()
-                            .createConfigurationFromArgs(newArgs, null, getKeyStoreClient());
-            proxyConfig.addFilesToClean(config.getFilesToClean());
-            return proxyConfig;
-        }
         if (isRetryCommand(config)) {
             return RetryConfigurationFactory.getInstance().createRetryConfiguration(config);
         }
         return config;
     }
 
+    /**
+     * Create a delegator based on the command line to see if we need to delegate the run.
+     *
+     * @throws ConfigurationException
+     */
+    public static TradefedDelegator checkDelegation(String[] args) throws ConfigurationException {
+        TradefedDelegator delegator = new TradefedDelegator();
+        ArgsOptionParser argsParser = new ArgsOptionParser(delegator);
+        List<String> argsList = new ArrayList<>(Arrays.asList(args));
+        argsList.remove(0);
+        argsParser.parseBestEffort(argsList, true);
+        return delegator;
+    }
+
     private void setDelegateLevelReporting(IConfiguration config) {
         List<ITestInvocationListener> delegateReporters = new ArrayList<>();
         // For debugging in the console, add a printer
@@ -1498,8 +1536,9 @@
 
         ExecutableCommand execCmd = createExecutableCommand(cmdTracker, config, false);
         context.setConfigurationDescriptor(config.getConfigurationDescription());
-        Map<String, ITestDevice> devices = allocateDevices(config, manager);
-        if (!devices.isEmpty()) {
+        DeviceAllocationResult allocationResults = allocateDevices(config, manager);
+        if (allocationResults.wasAllocationSuccessful()) {
+            Map<String, ITestDevice> devices = allocationResults.getAllocatedDevices();
             context.addAllocatedDevice(devices);
             synchronized (this) {
                 mExecutingCommands.add(execCmd);
@@ -1507,8 +1546,16 @@
             CLog.d("Executing '%s' on '%s'", cmdTracker.getArgs()[0], devices);
             startInvocation(context, execCmd, listener, new FreeDeviceHandler(manager));
         } else {
+            // Log adb output just to help debug
+            String adbOutput =
+                    ((DeviceManager) GlobalConfiguration.getDeviceManagerInstance())
+                            .executeGlobalAdbCommand("devices");
+            CLog.e("'adb devices' output:\n%s", adbOutput);
             throw new NoDeviceException(
-                    "no devices is available for command: " + Arrays.asList(args));
+                    String.format(
+                            "no devices is available for command: %s\n%s",
+                            Arrays.asList(args), allocationResults.formattedReason()),
+                    InfraErrorIdentifier.SCHEDULER_ALLOCATION_ERROR);
         }
     }
 
@@ -1549,11 +1596,12 @@
 
     /**
      * Allocate devices for a config.
+     *
      * @param config a {@link IConfiguration} has device requirements.
      * @param manager a {@link IDeviceManager}
      * @return allocated devices
      */
-    Map<String, ITestDevice> allocateDevices(IConfiguration config, IDeviceManager manager) {
+    DeviceAllocationResult allocateDevices(IConfiguration config, IDeviceManager manager) {
         Map<String, ITestDevice> devices = new LinkedHashMap<String, ITestDevice>();
         ITestDevice device = null;
         if (config.getDeviceConfig().isEmpty()) {
@@ -1562,6 +1610,7 @@
         // If we need to replicate the setup on all devices
         ParentShardReplicate.replicatedSetup(config, getKeyStoreClient());
         synchronized(this) {
+            DeviceAllocationResult allocationResults = new DeviceAllocationResult();
             for (IDeviceConfiguration deviceConfig : config.getDeviceConfig()) {
                 device =
                         manager.allocateDevice(
@@ -1569,6 +1618,9 @@
                 if (device != null) {
                     devices.put(deviceConfig.getDeviceName(), device);
                 } else {
+                    allocationResults.addAllocationFailureReason(
+                            deviceConfig.getDeviceName(),
+                            deviceConfig.getDeviceRequirements().getNoMatchReason());
                     // If one of the several device cannot be allocated, we de-allocate
                     // all the previous one.
                     for (ITestDevice allocatedDevice : devices.values()) {
@@ -1587,7 +1639,8 @@
                     break;
                 }
             }
-            return devices;
+            allocationResults.addAllocatedDevices(devices);
+            return allocationResults;
         }
     }
 
diff --git a/src/com/android/tradefed/command/DeviceAllocationResult.java b/src/com/android/tradefed/command/DeviceAllocationResult.java
new file mode 100644
index 0000000..849b04a
--- /dev/null
+++ b/src/com/android/tradefed/command/DeviceAllocationResult.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.command;
+
+import com.android.tradefed.device.ITestDevice;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/** Represents the results of an allocation attempt for a command. */
+public class DeviceAllocationResult {
+
+    private Map<String, String> mNotAllocatedReason = new LinkedHashMap<>();
+    private Map<String, ITestDevice> mAllocatedDevices = new LinkedHashMap<>();
+
+    /** returns whether or not the allocation was successful. */
+    public boolean wasAllocationSuccessful() {
+        return !mAllocatedDevices.isEmpty();
+    }
+
+    /** Add devices that have been allocated. */
+    public void addAllocatedDevices(Map<String, ITestDevice> devices) {
+        mAllocatedDevices.putAll(devices);
+    }
+
+    /** Add the reasons for not being allocated for each device config. */
+    public void addAllocationFailureReason(String deviceConfigName, Map<String, String> reasons) {
+        mNotAllocatedReason.put(deviceConfigName, createReasonMessage(reasons));
+    }
+
+    /** Returns the map of allocated devices */
+    public Map<String, ITestDevice> getAllocatedDevices() {
+        return mAllocatedDevices;
+    }
+
+    public String formattedReason() {
+        if (mNotAllocatedReason.size() == 1) {
+            return mNotAllocatedReason.values().iterator().next().toString();
+        }
+        return mNotAllocatedReason.toString();
+    }
+
+    private String createReasonMessage(Map<String, String> reasons) {
+        StringBuilder sb = new StringBuilder();
+        for (String serial : reasons.keySet()) {
+            String reason = reasons.get(serial);
+            if (reason == null) {
+                reason = "No reason provided";
+            }
+            sb.append(String.format("device '%s': %s", serial, reason));
+        }
+        return sb.toString();
+    }
+}
diff --git a/src/com/android/tradefed/command/ICommandOptions.java b/src/com/android/tradefed/command/ICommandOptions.java
index 37dd022..ea2ce99 100644
--- a/src/com/android/tradefed/command/ICommandOptions.java
+++ b/src/com/android/tradefed/command/ICommandOptions.java
@@ -19,6 +19,7 @@
 import com.android.tradefed.device.metric.AutoLogCollector;
 import com.android.tradefed.util.UniqueMultiMap;
 
+import java.time.Duration;
 import java.util.Map;
 import java.util.Set;
 
@@ -120,6 +121,10 @@
      */
     public void setInvocationTimeout(Long mInvocationTimeout);
 
+
+    /** Returns true if we should optimize the list of test modules for mainline test. */
+    public boolean getOptimizeMainlineTest();
+
     /**
      * Return the total shard count for the command.
      */
@@ -185,6 +190,12 @@
     /** Whether or not to attempt parallel setup of the remote devices. */
     public boolean shouldUseParallelRemoteSetup();
 
+    /** Whether or not to attempt parallel setup. */
+    public boolean shouldUseParallelSetup();
+
+    /** Returns the timeout to use during parallel setups. */
+    public Duration getParallelSetupTimeout();
+
     /** Whether or not to use replicated setup for all the remote devices. */
     public boolean shouldUseReplicateSetup();
 
diff --git a/src/com/android/tradefed/config/Configuration.java b/src/com/android/tradefed/config/Configuration.java
index e84fa3f..51d71c8 100644
--- a/src/com/android/tradefed/config/Configuration.java
+++ b/src/com/android/tradefed/config/Configuration.java
@@ -67,6 +67,7 @@
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /**
@@ -104,7 +105,8 @@
     // regexp pattern used to parse map option values
     private static final Pattern OPTION_KEY_VALUE_PATTERN = Pattern.compile("(?<!\\\\)=");
 
-    private static final String CONFIG_EXCEPTION_PATTERN = "Could not find option with name ";
+    private static final Pattern CONFIG_EXCEPTION_PATTERN =
+            Pattern.compile("Could not find option with name '(.*)'");
 
     /** Mapping of config object type name to config objects. */
     private Map<String, List<Object>> mConfigMap;
@@ -1123,10 +1125,11 @@
         try {
             return parser.parse(listArgs);
         } catch (ConfigurationException e) {
-            if (!e.getMessage().contains(CONFIG_EXCEPTION_PATTERN)) {
+            Matcher m = CONFIG_EXCEPTION_PATTERN.matcher(e.getMessage());
+            if (!m.matches()) {
                 throw e;
             }
-            String optionName = e.getMessage().split(CONFIG_EXCEPTION_PATTERN)[1];
+            String optionName = m.group(1);
             try {
                 // In case the option exists in the config descriptor, we change the error message
                 // to be more specific about why the option is rejected.
@@ -1138,7 +1141,7 @@
             }
             throw new OptionNotAllowedException(
                     String.format(
-                            "Option %s cannot be specified via "
+                            "Option '%s' cannot be specified via "
                                     + "command line. Only in the configuration xml.",
                             optionName));
         }
diff --git a/src/com/android/tradefed/config/DynamicRemoteFileResolver.java b/src/com/android/tradefed/config/DynamicRemoteFileResolver.java
index 9bfc06c..ae2ee62 100644
--- a/src/com/android/tradefed/config/DynamicRemoteFileResolver.java
+++ b/src/com/android/tradefed/config/DynamicRemoteFileResolver.java
@@ -380,7 +380,7 @@
             fileToResolve = new File(protocol + ":" + uri.getPath());
         } catch (URISyntaxException e) {
             CLog.e(e);
-            return null;
+            throw new BuildRetrievalError(e.getMessage(), e);
         }
 
         try {
diff --git a/src/com/android/tradefed/config/OptionSetter.java b/src/com/android/tradefed/config/OptionSetter.java
index 49d0bec..6b08ec32 100644
--- a/src/com/android/tradefed/config/OptionSetter.java
+++ b/src/com/android/tradefed/config/OptionSetter.java
@@ -330,8 +330,8 @@
     private OptionFieldsForName fieldsForArg(String name) throws ConfigurationException {
         OptionFieldsForName fields = fieldsForArgNoThrow(name);
         if (fields == null) {
-            throw new ConfigurationException(String.format("Could not find option with name %s",
-                    name));
+            throw new ConfigurationException(
+                    String.format("Could not find option with name '%s'", name));
         }
         return fields;
     }
diff --git a/src/com/android/tradefed/config/proxy/ProxyConfiguration.java b/src/com/android/tradefed/config/proxy/ProxyConfiguration.java
index bfe631d..86b7e50 100644
--- a/src/com/android/tradefed/config/proxy/ProxyConfiguration.java
+++ b/src/com/android/tradefed/config/proxy/ProxyConfiguration.java
@@ -15,31 +15,58 @@
  */
 package com.android.tradefed.config.proxy;
 
+import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.log.LogUtil.CLog;
 
 import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 
 /** Object that allows pointing to a remote configuration to execute. */
 public final class ProxyConfiguration {
 
     public static final String PROXY_CONFIG_TYPE_KEY = "proxy-config";
+    private static final String PROXY_CONFIG_OPTION_NAME = "proxy-configuration";
 
     @Option(
-            name = "proxy-configuration",
+            name = PROXY_CONFIG_OPTION_NAME,
             description = "Point to an external configuration to be run instead.")
     private File mProxyConfig;
 
+    /** Returns whether or not a proxy config value is set. */
+    public boolean isProxySet() {
+        return mProxyConfig != null;
+    }
+
     /** Returns the current proxy configuration to use. */
     public File getProxyConfig() {
         if (mProxyConfig == null || !mProxyConfig.exists()) {
-            CLog.d("No proxy configuration is configured.");
+            CLog.e("No proxy configuration is configured: %s", mProxyConfig);
             return null;
         }
         if (mProxyConfig.isDirectory()) {
-            CLog.w("Proxy configuration must be a file, found a directory: %s", mProxyConfig);
+            CLog.e("Proxy configuration must be a file, found a directory: %s", mProxyConfig);
             return null;
         }
         return mProxyConfig;
     }
+
+    public static String[] clearCommandline(String[] originalCommand)
+            throws ConfigurationException {
+        List<String> argsList = new ArrayList<>(Arrays.asList(originalCommand));
+        try {
+            while (argsList.contains("--" + PROXY_CONFIG_OPTION_NAME)) {
+                int index = argsList.indexOf("--" + PROXY_CONFIG_OPTION_NAME);
+                if (index != -1) {
+                    argsList.remove(index + 1);
+                    argsList.remove(index);
+                }
+            }
+        } catch (RuntimeException e) {
+            throw new ConfigurationException(e.getMessage(), e);
+        }
+        return argsList.toArray(new String[0]);
+    }
 }
diff --git a/src/com/android/tradefed/config/proxy/TradefedDelegator.java b/src/com/android/tradefed/config/proxy/TradefedDelegator.java
index d50c8a1..17af019 100644
--- a/src/com/android/tradefed/config/proxy/TradefedDelegator.java
+++ b/src/com/android/tradefed/config/proxy/TradefedDelegator.java
@@ -15,16 +15,20 @@
  */
 package com.android.tradefed.config.proxy;
 
+import com.android.tradefed.command.CommandOptions;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.Option;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.UniqueMultiMap;
 
 import com.google.common.base.Joiner;
 
 import java.io.File;
-import java.io.FileFilter;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
 
 /** Objects that helps delegating the invocation to another Tradefed binary. */
 public class TradefedDelegator {
@@ -40,6 +44,11 @@
                     "Points to the root dir of another Tradefed binary that will be used to drive the invocation")
     private File mDelegatedTfRootDir;
 
+    @Option(
+            name = CommandOptions.INVOCATION_DATA,
+            description = "Mirror of CommandOptions#INVOCATION_DATA")
+    private UniqueMultiMap<String, String> mInvocationData = new UniqueMultiMap<>();
+
     private String[] mCommandLine = null;
 
     /** Whether or not trigger the delegation logic. */
@@ -53,16 +62,8 @@
     }
 
     /** Creates the classpath out of the jars in the directory. */
-    public String createClasspath() {
-        List<File> jars =
-                Arrays.asList(
-                        mDelegatedTfRootDir.listFiles(
-                                new FileFilter() {
-                                    @Override
-                                    public boolean accept(File pathname) {
-                                        return pathname.getName().endsWith(".jar");
-                                    }
-                                }));
+    public String createClasspath() throws IOException {
+        Set<File> jars = FileUtil.findFilesObject(mDelegatedTfRootDir, ".*\\.jar");
         return Joiner.on(":").join(jars);
     }
 
@@ -74,12 +75,32 @@
         return mCommandLine;
     }
 
+    /**
+     * Returns whether or not this is the staging environment. We do not want to delegate in staging
+     * by default, only if the "staging_delegated" is set.
+     */
+    public boolean isStaging() {
+        return mInvocationData.containsKey("staging")
+                && !mInvocationData.containsKey("staging_delegated");
+    }
+
+    /**
+     * Remove from the original command line the delegate options so the underlying config does not
+     * delegate again.
+     */
     public static String[] clearCommandline(String[] originalCommand)
             throws ConfigurationException {
+        String[] commandLine = clearCommandlineFromOneArg(originalCommand, DELETEGATED_OPTION_NAME);
+        return commandLine;
+    }
+
+    /** Remove a given option from the command line. */
+    private static String[] clearCommandlineFromOneArg(String[] originalCommand, String optionName)
+            throws ConfigurationException {
         List<String> argsList = new ArrayList<>(Arrays.asList(originalCommand));
         try {
-            while (argsList.contains("--" + DELETEGATED_OPTION_NAME)) {
-                int index = argsList.indexOf("--" + DELETEGATED_OPTION_NAME);
+            while (argsList.contains("--" + optionName)) {
+                int index = argsList.indexOf("--" + optionName);
                 if (index != -1) {
                     argsList.remove(index + 1);
                     argsList.remove(index);
diff --git a/src/com/android/tradefed/config/remote/GcsRemoteFileResolver.java b/src/com/android/tradefed/config/remote/GcsRemoteFileResolver.java
index 64ba61f..68e94cd 100644
--- a/src/com/android/tradefed/config/remote/GcsRemoteFileResolver.java
+++ b/src/com/android/tradefed/config/remote/GcsRemoteFileResolver.java
@@ -20,6 +20,7 @@
 import com.android.tradefed.build.gcs.GCSDownloaderHelper;
 import com.android.tradefed.config.DynamicRemoteFileResolver;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
 
 import java.io.File;
 import java.io.IOException;
@@ -44,10 +45,12 @@
             File downloadedFile = getDownloader().fetchTestResource(path);
             // Unzip it if required
             return DynamicRemoteFileResolver.unzipIfRequired(downloadedFile, query);
-        } catch (BuildRetrievalError | IOException e) {
+        } catch (IOException e) {
             CLog.e(e);
             throw new BuildRetrievalError(
-                    String.format("Failed to download %s due to: %s", path, e.getMessage()), e);
+                    String.format("Failed to download %s due to: %s", path, e.getMessage()),
+                    e,
+                    InfraErrorIdentifier.GCS_ERROR);
         }
     }
 
diff --git a/src/com/android/tradefed/config/yaml/ConfigurationYamlParser.java b/src/com/android/tradefed/config/yaml/ConfigurationYamlParser.java
index 1aef60c..7a84771 100644
--- a/src/com/android/tradefed/config/yaml/ConfigurationYamlParser.java
+++ b/src/com/android/tradefed/config/yaml/ConfigurationYamlParser.java
@@ -21,6 +21,7 @@
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.config.yaml.IDefaultObjectLoader.LoaderConfiguration;
+import com.android.tradefed.config.yaml.YamlClassOptionsParser.ClassAndOptions;
 
 import com.google.common.collect.ImmutableList;
 
@@ -40,6 +41,8 @@
 public final class ConfigurationYamlParser {
 
     private static final String DESCRIPTION_KEY = "description";
+    public static final String PRE_SETUP_ACTION_KEY = "pre_setup_action";
+    public static final String POST_SETUP_ACTION_KEY = "post_setup_action";
     public static final String DEPENDENCIES_KEY = "dependencies";
     public static final String TESTS_KEY = "tests";
 
@@ -89,6 +92,16 @@
             mSeenKeys.add(DESCRIPTION_KEY);
         }
         Set<String> dependencyFiles = new LinkedHashSet<>();
+        if (yamlObjects.containsKey(PRE_SETUP_ACTION_KEY)) {
+            YamlClassOptionsParser classAndOptions =
+                    new YamlClassOptionsParser(
+                            "action",
+                            PRE_SETUP_ACTION_KEY,
+                            (List<Map<String, Object>>) yamlObjects.get(PRE_SETUP_ACTION_KEY));
+            mSeenKeys.add(PRE_SETUP_ACTION_KEY);
+            convertClassAndOptionsToObjects(
+                    configDef, classAndOptions, Configuration.TARGET_PREPARER_TYPE_NAME);
+        }
         if (yamlObjects.containsKey(DEPENDENCIES_KEY)) {
             YamlTestDependencies testDeps =
                     new YamlTestDependencies(
@@ -97,12 +110,24 @@
             mSeenKeys.add(DEPENDENCIES_KEY);
         }
         if (yamlObjects.containsKey(TESTS_KEY)) {
-            YamlTestRunners runnerInfo =
-                    new YamlTestRunners((List<Map<String, Object>>) yamlObjects.get(TESTS_KEY));
+            YamlClassOptionsParser runnerInfo =
+                    new YamlClassOptionsParser(
+                            "test",
+                            TESTS_KEY,
+                            (List<Map<String, Object>>) yamlObjects.get(TESTS_KEY));
             mSeenKeys.add(TESTS_KEY);
-            convertTestsToObjects(configDef, runnerInfo);
+            convertClassAndOptionsToObjects(configDef, runnerInfo, Configuration.TEST_TYPE_NAME);
         }
-
+        if (yamlObjects.containsKey(POST_SETUP_ACTION_KEY)) {
+            YamlClassOptionsParser runnerInfo =
+                    new YamlClassOptionsParser(
+                            "action",
+                            POST_SETUP_ACTION_KEY,
+                            (List<Map<String, Object>>) yamlObjects.get(POST_SETUP_ACTION_KEY));
+            mSeenKeys.add(POST_SETUP_ACTION_KEY);
+            convertClassAndOptionsToObjects(
+                    configDef, runnerInfo, Configuration.TARGET_PREPARER_TYPE_NAME);
+        }
         if (!mSeenKeys.containsAll(REQUIRED_KEYS)) {
             Set<String> missingKeys = new HashSet<>(REQUIRED_KEYS);
             missingKeys.removeAll(mSeenKeys);
@@ -185,27 +210,26 @@
         return dependencies;
     }
 
-    private void convertTestsToObjects(ConfigurationDef def, YamlTestRunners tests) {
-        if (tests.getRunner() == null) {
+    private void convertClassAndOptionsToObjects(
+            ConfigurationDef def, YamlClassOptionsParser tests, String configObjType) {
+        if (tests.getClassesAndOptions().isEmpty()) {
             return;
         }
-        String className = tests.getRunner();
-        int classCount = def.addConfigObjectDef(Configuration.TEST_TYPE_NAME, className);
-        for (Entry<String, String> options : tests.getOptions().entries()) {
-            String optionName =
-                    String.format(
-                            "%s%c%d%c%s",
-                            className,
-                            OptionSetter.NAMESPACE_SEPARATOR,
-                            classCount,
-                            OptionSetter.NAMESPACE_SEPARATOR,
-                            options.getKey());
-            def.addOptionDef(
-                    optionName,
-                    null,
-                    options.getValue(),
-                    def.getName(),
-                    Configuration.TEST_TYPE_NAME);
+        for (ClassAndOptions classOptions : tests.getClassesAndOptions()) {
+            String className = classOptions.mClass;
+            int classCount = def.addConfigObjectDef(configObjType, className);
+            for (Entry<String, String> options : classOptions.mOptions.entries()) {
+                String optionName =
+                        String.format(
+                                "%s%c%d%c%s",
+                                className,
+                                OptionSetter.NAMESPACE_SEPARATOR,
+                                classCount,
+                                OptionSetter.NAMESPACE_SEPARATOR,
+                                options.getKey());
+                def.addOptionDef(
+                        optionName, null, options.getValue(), def.getName(), configObjType);
+            }
         }
     }
 }
diff --git a/src/com/android/tradefed/config/yaml/YamlClassOptionsParser.java b/src/com/android/tradefed/config/yaml/YamlClassOptionsParser.java
new file mode 100644
index 0000000..3dcdae1
--- /dev/null
+++ b/src/com/android/tradefed/config/yaml/YamlClassOptionsParser.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.config.yaml;
+
+import com.android.tradefed.config.ConfigurationException;
+
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/** Helper to parse test runner information from the YAML Tradefed Configuration. */
+public class YamlClassOptionsParser {
+
+    private static final String CLASS_NAME_KEY = "name";
+    private static final String OPTIONS_KEY = "options";
+
+    class ClassAndOptions {
+        public String mClass;
+        public Multimap<String, String> mOptions = LinkedListMultimap.create();
+    }
+
+    private List<ClassAndOptions> mListClassAndOptions = new ArrayList<>();
+
+    public YamlClassOptionsParser(String mainkey, String category, List<Map<String, Object>> tests)
+            throws ConfigurationException {
+        for (Map<String, Object> runnerEntry : tests) {
+            if (runnerEntry.containsKey(mainkey)) {
+                ClassAndOptions classOptions = new ClassAndOptions();
+                mListClassAndOptions.add(classOptions);
+                for (Entry<String, Object> entry :
+                        ((Map<String, Object>) runnerEntry.get(mainkey)).entrySet()) {
+                    if (CLASS_NAME_KEY.equals(entry.getKey())) {
+                        classOptions.mClass = (String) entry.getValue();
+                    }
+                    if (OPTIONS_KEY.equals(entry.getKey())) {
+                        for (Map<String, Object> optionMap :
+                                (List<Map<String, Object>>) entry.getValue()) {
+                            for (Entry<String, Object> optionVal : optionMap.entrySet()) {
+                                // TODO: Support map option
+                                classOptions.mOptions.put(
+                                        optionVal.getKey(), optionVal.getValue().toString());
+                            }
+                        }
+                    }
+                }
+            } else {
+                throw new ConfigurationException(
+                        String.format("'%s' key is mandatory in '%s'", mainkey, category));
+            }
+        }
+    }
+
+    public List<ClassAndOptions> getClassesAndOptions() {
+        return mListClassAndOptions;
+    }
+}
diff --git a/src/com/android/tradefed/config/yaml/YamlTestRunners.java b/src/com/android/tradefed/config/yaml/YamlTestRunners.java
deleted file mode 100644
index 1c8952e..0000000
--- a/src/com/android/tradefed/config/yaml/YamlTestRunners.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.config.yaml;
-
-import com.android.tradefed.config.ConfigurationException;
-
-import com.google.common.collect.LinkedListMultimap;
-import com.google.common.collect.Multimap;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-
-/** Helper to parse test runner information from the YAML Tradefed Configuration. */
-public class YamlTestRunners {
-
-    private static final String TEST_KEY = "test";
-    private static final String TEST_NAME_KEY = "name";
-    private static final String OPTIONS_KEY = "options";
-
-    private String mRunner;
-    private Multimap<String, String> mOptions = LinkedListMultimap.create();
-
-    public YamlTestRunners(List<Map<String, Object>> tests) throws ConfigurationException {
-        if (tests.size() > 1) {
-            throw new ConfigurationException("Currently only support one runner at a time.");
-        }
-        for (Map<String, Object> runnerEntry : tests) {
-            if (runnerEntry.containsKey(TEST_KEY)) {
-                for (Entry<String, Object> entry :
-                        ((Map<String, Object>) runnerEntry.get(TEST_KEY)).entrySet()) {
-                    if (TEST_NAME_KEY.equals(entry.getKey())) {
-                        mRunner = (String) entry.getValue();
-                    }
-                    if (OPTIONS_KEY.equals(entry.getKey())) {
-                        for (Map<String, Object> optionMap :
-                                (List<Map<String, Object>>) entry.getValue()) {
-                            for (Entry<String, Object> optionVal : optionMap.entrySet()) {
-                                // TODO: Support map option
-                                mOptions.put(optionVal.getKey(), optionVal.getValue().toString());
-                            }
-                        }
-                    }
-                }
-            } else {
-                throw new ConfigurationException(
-                        String.format(
-                                "'%s' key is mandatory in '%s'",
-                                TEST_KEY, ConfigurationYamlParser.TESTS_KEY));
-            }
-        }
-    }
-
-    /** Returns the test runner to be used. */
-    public String getRunner() {
-        return mRunner;
-    }
-
-    /** Returns the options for the test runner */
-    public Multimap<String, String> getOptions() {
-        return mOptions;
-    }
-}
diff --git a/src/com/android/tradefed/device/DeviceManager.java b/src/com/android/tradefed/device/DeviceManager.java
index cf20f87..6694960 100644
--- a/src/com/android/tradefed/device/DeviceManager.java
+++ b/src/com/android/tradefed/device/DeviceManager.java
@@ -34,6 +34,7 @@
 import com.android.tradefed.log.ILogRegistry.EventType;
 import com.android.tradefed.log.LogRegistry;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.util.ArrayUtil;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
@@ -1016,8 +1017,10 @@
         @Override
         public void recoverDevice(IDeviceStateMonitor monitor, boolean recoverUntilOnline)
                 throws DeviceNotAvailableException {
-            throw new DeviceNotAvailableException("aborted test session",
-                    monitor.getSerialNumber());
+            throw new DeviceNotAvailableException(
+                    "aborted test session",
+                    monitor.getSerialNumber(),
+                    InfraErrorIdentifier.INVOCATION_CANCELLED);
         }
 
         /**
@@ -1026,8 +1029,10 @@
         @Override
         public void recoverDeviceBootloader(IDeviceStateMonitor monitor)
                 throws DeviceNotAvailableException {
-            throw new DeviceNotAvailableException("aborted test session",
-                    monitor.getSerialNumber());
+            throw new DeviceNotAvailableException(
+                    "aborted test session",
+                    monitor.getSerialNumber(),
+                    InfraErrorIdentifier.INVOCATION_CANCELLED);
         }
 
         /**
@@ -1036,8 +1041,10 @@
         @Override
         public void recoverDeviceRecovery(IDeviceStateMonitor monitor)
                 throws DeviceNotAvailableException {
-            throw new DeviceNotAvailableException("aborted test session",
-                    monitor.getSerialNumber());
+            throw new DeviceNotAvailableException(
+                    "aborted test session",
+                    monitor.getSerialNumber(),
+                    InfraErrorIdentifier.INVOCATION_CANCELLED);
         }
 
         /** {@inheritDoc} */
@@ -1045,7 +1052,9 @@
         public void recoverDeviceFastbootd(IDeviceStateMonitor monitor)
                 throws DeviceNotAvailableException {
             throw new DeviceNotAvailableException(
-                    "aborted test session", monitor.getSerialNumber());
+                    "aborted test session",
+                    monitor.getSerialNumber(),
+                    InfraErrorIdentifier.INVOCATION_CANCELLED);
         }
     }
 
diff --git a/src/com/android/tradefed/device/DeviceSelectionOptions.java b/src/com/android/tradefed/device/DeviceSelectionOptions.java
index 6dbae6d..fac18c7 100644
--- a/src/com/android/tradefed/device/DeviceSelectionOptions.java
+++ b/src/com/android/tradefed/device/DeviceSelectionOptions.java
@@ -29,6 +29,7 @@
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
@@ -161,6 +162,8 @@
 
     // If we have tried to fetch the environment variable ANDROID_SERIAL before.
     private boolean mFetchedEnvVariable = false;
+    // Store the reason for which the device was not matched.
+    private Map<String, String> mNoMatchReason = new LinkedHashMap<>();
 
     private static final String VARIANT_SEPARATOR = ":";
 
@@ -448,6 +451,7 @@
      */
     @Override
     public boolean matches(IDevice device) {
+        String deviceSerial = device.getSerialNumber();
         Collection<String> serials = getSerials(device);
         Collection<String> excludeSerials = getExcludeSerials();
         Map<String, Collection<String>> productVariants = splitOnVariant(getProductTypes());
@@ -456,9 +460,17 @@
 
         if (!serials.isEmpty() &&
                 !serials.contains(device.getSerialNumber())) {
+            addNoMatchReason(
+                    deviceSerial,
+                    String.format(
+                            "device serial does not match any requested serial(%s)", serials));
             return false;
         }
         if (excludeSerials.contains(device.getSerialNumber())) {
+            addNoMatchReason(
+                    deviceSerial,
+                    String.format(
+                            "device serial was part of excluded serials(%s)", excludeSerials));
             return false;
         }
         if (!productTypes.isEmpty()) {
@@ -468,15 +480,31 @@
                 String productVariant = getDeviceProductVariant(device);
                 Collection<String> variants = productVariants.get(productType);
                 if (variants != null && !variants.contains(productVariant)) {
+                    addNoMatchReason(
+                            deviceSerial,
+                            String.format(
+                                    "device variant (%s) does not match requested variants(%s)",
+                                    productVariant, variants));
                     return false;
                 }
             } else {
                 // no product type matches; bye-bye
+                addNoMatchReason(
+                        deviceSerial,
+                        String.format(
+                                "device product type (%s) does not match requested product types(%s)",
+                                productType, productTypes));
                 return false;
             }
         }
         for (Map.Entry<String, String> propEntry : properties.entrySet()) {
-            if (!propEntry.getValue().equals(device.getProperty(propEntry.getKey()))) {
+            String deviceProperty = device.getProperty(propEntry.getKey());
+            if (!propEntry.getValue().equals(deviceProperty)) {
+                addNoMatchReason(
+                        deviceSerial,
+                        String.format(
+                                "device property (%s) value(%s) does not match requested value(%s)",
+                                propEntry.getKey(), deviceProperty, propEntry.getValue()));
                 return false;
             }
         }
@@ -488,12 +516,25 @@
         if ((mMinSdk != null) || (mMaxSdk != null)) {
             int deviceSdkLevel = getDeviceSdkLevel(device);
             if (deviceSdkLevel < 0) {
+                addNoMatchReason(
+                        deviceSerial,
+                        String.format("device returned unexpected sdk level (%s)", deviceSdkLevel));
                 return false;
             }
             if (mMinSdk != null && deviceSdkLevel < mMinSdk) {
+                addNoMatchReason(
+                        deviceSerial,
+                        String.format(
+                                "device sdk (%s) is below the requested min sdk (%s)",
+                                deviceSdkLevel, mMinSdk));
                 return false;
             }
             if (mMaxSdk != null && mMaxSdk < deviceSdkLevel) {
+                addNoMatchReason(
+                        deviceSerial,
+                        String.format(
+                                "device sdk (%s) is above the requested max sdk (%s)",
+                                deviceSdkLevel, mMaxSdk));
                 return false;
             }
         }
@@ -505,19 +546,35 @@
                 if (device instanceof StubDevice || device instanceof FastbootDevice) {
                     // Reading battery of fastboot and StubDevice device does not work and could
                     // lead to weird log.
+                    addNoMatchReason(
+                            deviceSerial,
+                            String.format(
+                                    "device type is (%s) which cannot have a battery required.",
+                                    device.getClass()));
                     return false;
                 }
                 Integer deviceBattery = getBatteryLevel(device);
                 if (deviceBattery == null) {
                     // Couldn't determine battery level when that check is required; reject device
+                    addNoMatchReason(deviceSerial, "device failed to return a battery reading.");
                     return false;
                 }
                 if (isLessAndNotNull(deviceBattery, mMinBattery)) {
                     // deviceBattery < mMinBattery
+                    addNoMatchReason(
+                            deviceSerial,
+                            String.format(
+                                    "device battery (%s) is below the requested min battery (%s)",
+                                    deviceBattery, mMinBattery));
                     return false;
                 }
                 if (isLessEqAndNotNull(mMaxBattery, deviceBattery)) {
                     // mMaxBattery <= deviceBattery
+                    addNoMatchReason(
+                            deviceSerial,
+                            String.format(
+                                    "device battery (%s) is above the requested max battery (%s)",
+                                    deviceBattery, mMaxBattery));
                     return false;
                 }
             }
@@ -558,39 +615,55 @@
         if ((emulatorRequested() || stubEmulatorRequested()) && !device.isEmulator()) {
             return false;
         }
+        String deviceSerial = device.getSerialNumber();
         // If physical device is requested but device is emulator or remote ip device, skip
         if (deviceRequested()
                 && (device.isEmulator()
                         || RemoteAndroidDevice.checkSerialFormatValid(device.getSerialNumber()))) {
+            addNoMatchReason(deviceSerial, "device is not a physical device");
             return false;
         }
 
         if (mRequestedType != null) {
             Class<?> classNeeded = mRequestedType.getRequiredClass();
             if (!device.getClass().equals(classNeeded)) {
+                addNoMatchReason(
+                        deviceSerial,
+                        String.format(
+                                "device is type (%s) while requested type was (%s)",
+                                device.getClass(), classNeeded));
                 return false;
             }
         } else {
             if (device.isEmulator() && (device instanceof StubDevice) && !stubEmulatorRequested()) {
                 // only allocate the stub emulator if requested
+                addNoMatchReason(deviceSerial, "device is emulator while requested type was not");
                 return false;
             }
             if (nullDeviceRequested() != (device instanceof NullDevice)) {
+                addNoMatchReason(
+                        deviceSerial, "device is null-device while requested type was not");
                 return false;
             }
             if (tcpDeviceRequested() != TcpDevice.class.equals(device.getClass())) {
                 // We only match an exact TcpDevice here, no child class.
+                addNoMatchReason(deviceSerial, "device is tcp-device while requested type was not");
                 return false;
             }
             if (gceDeviceRequested() != RemoteAvdIDevice.class.equals(device.getClass())) {
                 // We only match an exact RemoteAvdIDevice here, no child class.
+                addNoMatchReason(deviceSerial, "device is gce-device while requested type was not");
                 return false;
             }
             if (remoteDeviceRequested() != VmRemoteDevice.class.equals(device.getClass())) {
+                addNoMatchReason(
+                        deviceSerial, "device is remote-device while requested type was not");
                 return false;
             }
             if (localVirtualDeviceRequested()
                     != StubLocalAndroidVirtualDevice.class.equals(device.getClass())) {
+                addNoMatchReason(
+                        deviceSerial, "device is local-virtual while requested type was not");
                 return false;
             }
         }
@@ -704,6 +777,15 @@
         return apiLevel;
     }
 
+    private void addNoMatchReason(String device, String reason) {
+        mNoMatchReason.put(device, reason);
+    }
+
+    @Override
+    public Map<String, String> getNoMatchReason() {
+        return mNoMatchReason;
+    }
+
     /**
      * Helper factory method to create a {@link IDeviceSelection} that will only match device
      * with given serial
diff --git a/src/com/android/tradefed/device/IDeviceSelection.java b/src/com/android/tradefed/device/IDeviceSelection.java
index c61179e..1302532 100644
--- a/src/com/android/tradefed/device/IDeviceSelection.java
+++ b/src/com/android/tradefed/device/IDeviceSelection.java
@@ -116,4 +116,10 @@
      */
     public void setSerial(String... serialNumber);
 
+    /**
+     * Returns the reason for which the device was not matched.
+     *
+     * @return a Map of serial number to reason for which it wasn't allocated
+     */
+    public Map<String, String> getNoMatchReason();
 }
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index fcd8635..e859431 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -107,6 +107,7 @@
 public class NativeDevice implements IManagedTestDevice {
 
     protected static final String SD_CARD = "/sdcard/";
+    protected static final String STORAGE_EMULATED = "/storage/emulated/";
     /**
      * Allow pauses of up to 2 minutes while receiving bugreport.
      * <p/>
@@ -1089,7 +1090,7 @@
     public boolean pullFile(final String remoteFilePath, final File localFile)
             throws DeviceNotAvailableException {
 
-        if (remoteFilePath.startsWith(SD_CARD)) {
+        if (isSdcardOrEmulated(remoteFilePath)) {
             ContentProviderHandler handler = getContentProvider();
             if (handler != null) {
                 return handler.pullFile(remoteFilePath, localFile);
@@ -1197,7 +1198,7 @@
     @Override
     public boolean pushFile(final File localFile, final String remoteFilePath)
             throws DeviceNotAvailableException {
-        if (remoteFilePath.startsWith(SD_CARD)) {
+        if (isSdcardOrEmulated(remoteFilePath)) {
             ContentProviderHandler handler = getContentProvider();
             if (handler != null) {
                 return handler.pushFile(localFile, remoteFilePath);
@@ -1272,6 +1273,15 @@
     /** {@inheritDoc} */
     @Override
     public boolean doesFileExist(String deviceFilePath) throws DeviceNotAvailableException {
+        if (isSdcardOrEmulated(deviceFilePath)) {
+            ContentProviderHandler handler = getContentProvider();
+            if (handler != null) {
+                CLog.d("Delegating check to ContentProvider doesFileExist(%s)", deviceFilePath);
+
+                return handler.doesFileExist(deviceFilePath);
+            }
+        }
+        CLog.d("Using 'ls' to check doesFileExist(%s)", deviceFilePath);
         String lsGrep = executeShellCommand(String.format("ls \"%s\"", deviceFilePath));
         return !lsGrep.contains("No such file or directory");
     }
@@ -1279,7 +1289,7 @@
     /** {@inheritDoc} */
     @Override
     public void deleteFile(String deviceFilePath) throws DeviceNotAvailableException {
-        if (deviceFilePath.startsWith(SD_CARD)) {
+        if (isSdcardOrEmulated(deviceFilePath)) {
             ContentProviderHandler handler = getContentProvider();
             if (handler != null) {
                 if (handler.deleteFile(deviceFilePath)) {
@@ -1612,7 +1622,7 @@
     @Override
     public boolean pullDir(String deviceFilePath, File localDir)
             throws DeviceNotAvailableException {
-        if (deviceFilePath.startsWith(SD_CARD)) {
+        if (isSdcardOrEmulated(deviceFilePath)) {
             ContentProviderHandler handler = getContentProvider();
             if (handler != null) {
                 return handler.pullDir(deviceFilePath, localDir);
@@ -1665,6 +1675,11 @@
         return true;
     }
 
+    /** Checks whether path is external storage path. */
+    private boolean isSdcardOrEmulated(String path) {
+        return path.startsWith(SD_CARD) || path.startsWith(STORAGE_EMULATED);
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -2099,9 +2114,13 @@
             recoverDevice();
         }
         if (retryAttempts > 0) {
-            throw new DeviceUnresponsiveException(String.format("Attempted %s multiple times "
-                    + "on device %s without communication success. Aborting.", actionDescription,
-                    getSerialNumber()), getSerialNumber());
+            throw new DeviceUnresponsiveException(
+                    String.format(
+                            "Attempted %s multiple times "
+                                    + "on device %s without communication success. Aborting.",
+                            actionDescription, getSerialNumber()),
+                    getSerialNumber(),
+                    DeviceErrorIdentifier.DEVICE_UNRESPONSIVE);
         }
         return false;
     }
@@ -3950,7 +3969,8 @@
             throw new DeviceRuntimeException(
                     String.format(
                             "Failed to query property '%s'. device returned null.",
-                            DeviceProperties.BUILD_CODENAME));
+                            DeviceProperties.BUILD_CODENAME),
+                    DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
         }
         codeName = codeName.trim();
         int apiLevel = getApiLevel() + ("REL".equals(codeName) ? 0 : 1);
diff --git a/src/com/android/tradefed/device/NoDeviceException.java b/src/com/android/tradefed/device/NoDeviceException.java
index 45f4339..a2fae93 100644
--- a/src/com/android/tradefed/device/NoDeviceException.java
+++ b/src/com/android/tradefed/device/NoDeviceException.java
@@ -16,36 +16,23 @@
 package com.android.tradefed.device;
 
 import com.android.tradefed.build.BuildSerializedVersion;
+import com.android.tradefed.error.HarnessRuntimeException;
+import com.android.tradefed.result.error.ErrorIdentifier;
 
-/**
- * Thrown when there's no device to execute a given command.
- */
-public class NoDeviceException extends Exception {
+import java.lang.StackWalker.Option;
+
+/** Thrown when there's no device to execute a given command. */
+public class NoDeviceException extends HarnessRuntimeException {
     private static final long serialVersionUID = BuildSerializedVersion.VERSION;
 
     /**
      * Creates a {@link NoDeviceException}.
-     */
-    public NoDeviceException() {
-        super();
-    }
-
-    /**
-     * Creates a {@link NoDeviceException}.
      *
      * @param msg a descriptive message.
+     * @param errorId The {@link ErrorIdentifier} categorizing the exception.
      */
-    public NoDeviceException(String msg) {
-        super(msg);
-    }
-
-    /**
-     * Creates a {@link NoDeviceException}.
-     *
-     * @param msg a descriptive message.
-     * @param cause the root {@link Throwable} that caused the device to become unavailable.
-     */
-    public NoDeviceException(String msg, Throwable cause) {
-        super(msg, cause);
+    public NoDeviceException(String msg, ErrorIdentifier errorId) {
+        super(msg, errorId);
+        setCallerClass(StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass());
     }
 }
diff --git a/src/com/android/tradefed/device/TestDevice.java b/src/com/android/tradefed/device/TestDevice.java
index 80fb98d..958f21d 100644
--- a/src/com/android/tradefed/device/TestDevice.java
+++ b/src/com/android/tradefed/device/TestDevice.java
@@ -27,6 +27,7 @@
 import com.android.tradefed.result.ByteArrayInputStreamSource;
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.InputStreamSource;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.util.AaptParser;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
@@ -1187,7 +1188,8 @@
         String[] lines = commandOutput.split("\\r?\\n");
         if (!lines[0].equals("Users:")) {
             throw new DeviceRuntimeException(
-                    String.format("'%s' in not a valid output for 'pm list users'", commandOutput));
+                    String.format("'%s' in not a valid output for 'pm list users'", commandOutput),
+                    DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
         }
         ArrayList<String[]> users = new ArrayList<String[]>(lines.length - 1);
         for (int i = 1; i < lines.length; i++) {
@@ -1199,7 +1201,8 @@
                         String.format(
                                 "device output: '%s' \nline: '%s' was not in the expected "
                                         + "format for user info.",
-                                commandOutput, lines[i]));
+                                commandOutput, lines[i]),
+                        DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
             }
             users.add(tokens);
         }
diff --git a/src/com/android/tradefed/device/cloud/GceManager.java b/src/com/android/tradefed/device/cloud/GceManager.java
index 58fd31d..a206884 100644
--- a/src/com/android/tradefed/device/cloud/GceManager.java
+++ b/src/com/android/tradefed/device/cloud/GceManager.java
@@ -26,6 +26,7 @@
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.targetprep.TargetSetupError;
 import com.android.tradefed.util.ArrayUtil;
 import com.android.tradefed.util.CommandResult;
@@ -218,7 +219,9 @@
                                     + "The instance may not have booted up at all.";
                     CLog.e(errors);
                     throw new TargetSetupError(
-                            String.format("acloud errors: %s", errors), mDeviceDescriptor);
+                            String.format("acloud errors: %s", errors),
+                            mDeviceDescriptor,
+                            InfraErrorIdentifier.NO_ACLOUD_REPORT);
                 }
             }
             mGceAvdInfo =
@@ -226,7 +229,11 @@
                             reportFile, mDeviceDescriptor, mDeviceOptions.getRemoteAdbPort());
             return mGceAvdInfo;
         } catch (IOException e) {
-            throw new TargetSetupError("failed to create log file", e, mDeviceDescriptor);
+            throw new TargetSetupError(
+                    "failed to create log file",
+                    e,
+                    mDeviceDescriptor,
+                    InfraErrorIdentifier.FAIL_TO_CREATE_FILE);
         } finally {
             FileUtil.deleteFile(reportFile);
         }
diff --git a/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java b/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
index 4925bb6..878e114 100644
--- a/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
+++ b/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
@@ -37,6 +37,8 @@
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.StreamUtil;
 
@@ -63,6 +65,7 @@
 
     private GceManager mGceHandler = null;
     private GceSshTunnelMonitor mGceSshMonitor;
+    private DeviceNotAvailableException mTunnelInitFailed = null;
 
     private static final long WAIT_FOR_TUNNEL_ONLINE = 2 * 60 * 1000;
     private static final long WAIT_AFTER_REBOOT = 60 * 1000;
@@ -91,6 +94,7 @@
         try {
             mGceAvd = null;
             mGceSshMonitor = null;
+            mTunnelInitFailed = null;
             // We create a brand new GceManager each time to ensure clean state.
             mGceHandler = new GceManager(getDeviceDescriptor(), getOptions(), info);
             getGceHandler().logStableHostImageInfos(info);
@@ -269,7 +273,10 @@
                         String.format(
                                 "Device failed to boot. Error from Acloud: %s",
                                 mGceAvd.getErrors());
-                throw new TargetSetupError(errorMsg, getDeviceDescriptor());
+                throw new TargetSetupError(
+                        errorMsg,
+                        getDeviceDescriptor(),
+                        DeviceErrorIdentifier.FAILED_TO_LAUNCH_GCE);
             }
         }
         createGceSshMonitor(this, buildInfo, mGceAvd.hostAndPort(), this.getOptions());
@@ -322,14 +329,20 @@
             }
             getRunUtil().sleep(RETRY_INTERVAL_MS);
         }
-        throw new DeviceNotAvailableException(
-                String.format("Tunnel did not come back online after %sms", waitTime),
-                getSerialNumber(),
-                DeviceErrorIdentifier.FAILED_TO_CONNECT_TO_GCE);
+        mTunnelInitFailed =
+                new DeviceNotAvailableException(
+                        String.format("Tunnel did not come back online after %sms", waitTime),
+                        getSerialNumber(),
+                        DeviceErrorIdentifier.FAILED_TO_CONNECT_TO_GCE);
+        throw mTunnelInitFailed;
     }
 
     @Override
     public void recoverDevice() throws DeviceNotAvailableException {
+        if (getGceSshMonitor() == null && mTunnelInitFailed != null) {
+            // We threw before but was not reported, so throw the root cause here.
+            throw mTunnelInitFailed;
+        }
         // Re-init tunnel when attempting recovery
         CLog.i("Attempting recovery on GCE AVD %s", getSerialNumber());
         getGceSshMonitor().closeConnection();
@@ -447,4 +460,38 @@
         }
         return descriptor;
     }
+
+    /**
+     * Attempt to powerwash a GCE instance
+     *
+     * @return returns true if powerwash Gce success.
+     * @throws TargetSetupError
+     * @throws DeviceNotAvailableException
+     */
+    public boolean powerwashGce() throws TargetSetupError, DeviceNotAvailableException {
+        if (mGceAvd == null) {
+            String errorMsg = String.format("Can not get GCE AVD Info. launch GCE first?");
+            throw new TargetSetupError(
+                    errorMsg, getDeviceDescriptor(), DeviceErrorIdentifier.DEVICE_UNAVAILABLE);
+        }
+        String username = this.getOptions().getInstanceUser();
+        String powerwashCommand = String.format("/home/%s/bin/powerwash_cvd", username);
+        CommandResult powerwashRes =
+                GceManager.remoteSshCommandExecution(
+                        mGceAvd,
+                        this.getOptions(),
+                        getRunUtil(),
+                        60000L,
+                        powerwashCommand.split(" "));
+        if (!CommandStatus.SUCCESS.equals(powerwashRes.getStatus())) {
+            CLog.e("%s", powerwashRes.getStderr());
+            // Log 'adb devices' to confirm device is gone
+            CommandResult printAdbDevices = getRunUtil().runTimedCmd(60000L, "adb", "devices");
+            CLog.e("%s\n%s", printAdbDevices.getStdout(), printAdbDevices.getStderr());
+            // Proceed here, device could have been already gone.
+            return false;
+        }
+        getMonitor().waitForDeviceAvailable();
+        return true;
+    }
 }
diff --git a/src/com/android/tradefed/device/metric/AutoLogCollector.java b/src/com/android/tradefed/device/metric/AutoLogCollector.java
index 41bbfdb..d93b377 100644
--- a/src/com/android/tradefed/device/metric/AutoLogCollector.java
+++ b/src/com/android/tradefed/device/metric/AutoLogCollector.java
@@ -20,7 +20,10 @@
 /** Enumeration describing which collector can automatically be handled by the harness. */
 public enum AutoLogCollector {
     BUGREPORTZ_ON_FAILURE(BugreportzOnFailureCollector.class),
+    CLANG_COVERAGE(ClangCodeCoverageCollector.class),
+    GCOV_COVERAGE(GcovCodeCoverageCollector.class),
     HOSTLOG_ON_FAILURE(DebugHostLogOnFailureCollector.class),
+    JAVA_COVERAGE(JavaCodeCoverageCollector.class),
     LOGCAT_ON_FAILURE(LogcatOnFailureCollector.class),
     SCREENSHOT_ON_FAILURE(ScreenshotOnFailureCollector.class);
 
diff --git a/test_framework/com/android/tradefed/testtype/ClangCodeCoverageListener.java b/src/com/android/tradefed/device/metric/ClangCodeCoverageCollector.java
similarity index 73%
rename from test_framework/com/android/tradefed/testtype/ClangCodeCoverageListener.java
rename to src/com/android/tradefed/device/metric/ClangCodeCoverageCollector.java
index b0fb295..9e6304a 100644
--- a/test_framework/com/android/tradefed/testtype/ClangCodeCoverageListener.java
+++ b/src/com/android/tradefed/device/metric/ClangCodeCoverageCollector.java
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.tradefed.testtype;
+package com.android.tradefed.device.metric;
 
-import static com.google.common.base.Verify.verify;
+import static com.android.tradefed.testtype.coverage.CoverageOptions.Toolchain.CLANG;
 import static com.google.common.base.Verify.verifyNotNull;
 
 import com.android.tradefed.build.BuildRetrievalError;
@@ -25,14 +25,13 @@
 import com.android.tradefed.config.IConfigurationReceiver;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.ResultForwarder;
-import com.android.tradefed.testtype.coverage.CoverageOptions;
-import com.android.tradefed.testtype.coverage.CoverageOptions.Toolchain;
+import com.android.tradefed.util.AdbRootElevator;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
@@ -48,15 +47,15 @@
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
- * A {@link ResultForwarder} that will pull Clang coverage measurements off of the device and log
- * them as test artifacts.
+ * A {@link com.android.tradefed.device.metric.BaseDeviceMetricCollector} that will pull Clang
+ * coverage measurements off of the device and log them as test artifacts.
  */
-public final class ClangCodeCoverageListener extends ResultForwarder
+public final class ClangCodeCoverageCollector extends BaseDeviceMetricCollector
         implements IConfigurationReceiver {
 
     private static final String NATIVE_COVERAGE_DEVICE_PATH = "/data/misc/trace";
@@ -74,21 +73,27 @@
     private static final String DELETE_COVERAGE_FILES_COMMAND =
             String.format("find %s -name '*.profraw' -delete", NATIVE_COVERAGE_DEVICE_PATH);
 
-    private final ITestDevice mDevice;
-
     private IBuildInfo mBuildInfo;
     private IConfiguration mConfiguration;
-    private IRunUtil mRunUtil;
+    private IRunUtil mRunUtil = RunUtil.getDefault();
 
     private NativeCodeCoverageFlusher mFlusher;
 
-    private File mLlvmProfdataTool;
-    private String mCurrentRunName;
+    @Override
+    public ITestInvocationListener init(
+            IInvocationContext context, ITestInvocationListener listener) {
+        super.init(context, listener);
 
-    public ClangCodeCoverageListener(ITestDevice device, ITestInvocationListener... listeners) {
-        super(listeners);
-        mDevice = device;
-        mRunUtil = RunUtil.getDefault();
+        if (isClangCoverageEnabled()) {
+            // Clear coverage measurements on the device.
+            try (AdbRootElevator adbRoot = new AdbRootElevator(getDevices().get(0))) {
+                getCoverageFlusher().resetCoverage();
+            } catch (DeviceNotAvailableException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        return this;
     }
 
     @Override
@@ -102,42 +107,26 @@
     }
 
     @Override
-    public void testRunStarted(String runName, int testCount) {
-        mCurrentRunName = runName;
-        super.testRunStarted(runName, testCount);
-    }
+    public void onTestRunEnd(
+            DeviceMetricData runData, final Map<String, Metric> currentRunMetrics) {
+        if (!isClangCoverageEnabled()) {
+            return;
+        }
 
-    @Override
-    public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
-        CoverageOptions options = mConfiguration.getCoverageOptions();
-        try {
-            if (options.isCoverageEnabled()
-                    && options.getCoverageToolchains().contains(Toolchain.CLANG)) {
-                // Enable abd root on the device, otherwise the following commands will fail.
-                verify(mDevice.enableAdbRoot(), "Failed to enable adb root.");
-
-                if (options.isCoverageFlushEnabled()) {
-                    getCoverageFlusher().forceCoverageFlush();
-                }
-                logCoverageMeasurement(mCurrentRunName);
-
-                // Delete coverage files on the device.
-                mDevice.executeShellCommand(DELETE_COVERAGE_FILES_COMMAND);
+        ITestDevice device = getRealDevices().get(0);
+        try (AdbRootElevator adbRoot = new AdbRootElevator(device)) {
+            if (mConfiguration.getCoverageOptions().isCoverageFlushEnabled()) {
+                getCoverageFlusher().forceCoverageFlush();
             }
+            logCoverageMeasurement(device, getRunName());
+
+            // Delete coverage files on the device.
+            device.executeShellCommand(DELETE_COVERAGE_FILES_COMMAND);
         } catch (DeviceNotAvailableException | IOException e) {
             throw new RuntimeException(e);
-        } finally {
-            super.testRunEnded(elapsedTime, runMetrics);
         }
     }
 
-    @Override
-    public void invocationEnded(long elapsedTime) {
-        // Clean up the llvm-profdata tool.
-        FileUtil.recursiveDelete(mLlvmProfdataTool);
-        super.invocationEnded(elapsedTime);
-    }
-
     /**
      * Logs Clang coverage measurements from the device.
      *
@@ -145,7 +134,7 @@
      * @throws DeviceNotAvailableException
      * @throws IOException
      */
-    private void logCoverageMeasurement(String runName)
+    private void logCoverageMeasurement(ITestDevice device, String runName)
             throws DeviceNotAvailableException, IOException {
         File coverageTarGz = null;
         File untarDir = null;
@@ -153,13 +142,13 @@
         File indexedProfileFile = null;
         try {
             // Compress coverage measurements on the device before pulling.
-            mDevice.executeShellCommand(ZIP_CLANG_FILES_COMMAND);
-            coverageTarGz = mDevice.pullFile(COVERAGE_TAR_PATH);
+            device.executeShellCommand(ZIP_CLANG_FILES_COMMAND);
+            coverageTarGz = device.pullFile(COVERAGE_TAR_PATH);
             verifyNotNull(
                     coverageTarGz,
                     "Failed to pull the Clang code coverage file %s",
                     COVERAGE_TAR_PATH);
-            mDevice.deleteFile(COVERAGE_TAR_PATH);
+            device.deleteFile(COVERAGE_TAR_PATH);
 
             untarDir = FileUtil.createTempDir("clang_coverage");
             TarUtil.unTar(coverageTarGz, untarDir);
@@ -208,6 +197,7 @@
         } finally {
             FileUtil.deleteFile(coverageTarGz);
             FileUtil.recursiveDelete(untarDir);
+            FileUtil.recursiveDelete(profileTool);
             FileUtil.deleteFile(indexedProfileFile);
         }
     }
@@ -220,25 +210,26 @@
     private NativeCodeCoverageFlusher getCoverageFlusher() {
         if (mFlusher == null) {
             verifyNotNull(mConfiguration);
-            verifyNotNull(mDevice);
             mFlusher =
                     new NativeCodeCoverageFlusher(
-                            mDevice, mConfiguration.getCoverageOptions().getCoverageProcesses());
+                            getDevices().get(0),
+                            mConfiguration.getCoverageOptions().getCoverageProcesses());
         }
         return mFlusher;
     }
 
+    private boolean isClangCoverageEnabled() {
+        return mConfiguration != null
+                && mConfiguration.getCoverageOptions().isCoverageEnabled()
+                && mConfiguration.getCoverageOptions().getCoverageToolchains().contains(CLANG);
+    }
+
     /**
      * Retrieves the profile tool and dependencies from the build, and extracts them.
      *
      * @return the directory containing the profile tool and dependencies
      */
     private File getProfileTool() throws IOException {
-        // If we have a cached version of the profile tool already, use it.
-        if (mLlvmProfdataTool != null) {
-            return mLlvmProfdataTool;
-        }
-
         // If llvm-profdata-path was set in the Configuration, pass it through. Don't save the path
         // locally since the parent process is responsible for cleaning it up.
         File configurationTool = mConfiguration.getCoverageOptions().getLlvmProfdataPath();
@@ -254,8 +245,7 @@
                     verifyNotNull(
                             buildInfo.getFile("llvm-profdata.zip"),
                             "Could not get llvm-profdata.zip from the build.");
-            mLlvmProfdataTool = ZipUtil.extractZipToTemp(profileToolZip, "llvm-profdata");
-            return mLlvmProfdataTool;
+            return ZipUtil.extractZipToTemp(profileToolZip, "llvm-profdata");
         } catch (BuildRetrievalError e) {
             throw new RuntimeException(e);
         } finally {
diff --git a/src/com/android/tradefed/device/metric/FilePullerLogCollector.java b/src/com/android/tradefed/device/metric/FilePullerLogCollector.java
index 75cb203..da8c240 100644
--- a/src/com/android/tradefed/device/metric/FilePullerLogCollector.java
+++ b/src/com/android/tradefed/device/metric/FilePullerLogCollector.java
@@ -42,9 +42,12 @@
                 String ext = FileUtil.getExtension(metricFile.getName()).toLowerCase();
                 if (".png".equals(ext)) {
                     type = LogDataType.PNG;
-                }
-                if (".pb".equals(ext)) {
+                } else if (".pb".equals(ext)) {
                     type = LogDataType.PB;
+                } else if (".mp4".equals(ext)) {
+                    type = LogDataType.MP4;
+                } else if (".hprof".equals(ext)) {
+                    type = LogDataType.HPROF;
                 }
                 testLog(metricFile.getName(), type, source);
             }
diff --git a/src/com/android/tradefed/device/metric/GcovCodeCoverageCollector.java b/src/com/android/tradefed/device/metric/GcovCodeCoverageCollector.java
new file mode 100644
index 0000000..0bd59e2
--- /dev/null
+++ b/src/com/android/tradefed/device/metric/GcovCodeCoverageCollector.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.device.metric;
+
+import static com.android.tradefed.testtype.coverage.CoverageOptions.Toolchain.GCOV;
+import static com.google.common.base.Verify.verifyNotNull;
+
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.IConfigurationReceiver;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.util.AdbRootElevator;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.NativeCodeCoverageFlusher;
+import com.android.tradefed.util.TarUtil;
+import com.android.tradefed.util.ZipUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * A {@link com.android.tradefed.device.metric.BaseDeviceMetricCollector} that will pull gcov
+ * coverage measurements off of the device and log them as test artifacts.
+ */
+public final class GcovCodeCoverageCollector extends BaseDeviceMetricCollector
+        implements IConfigurationReceiver {
+
+    private static final String NATIVE_COVERAGE_DEVICE_PATH = "/data/misc/trace";
+    private static final String COVERAGE_TAR_PATH =
+            String.format("%s/coverage.tar", NATIVE_COVERAGE_DEVICE_PATH);
+
+    // Finds .gcda files in /data/misc/trace and compresses those files only. Stores the full
+    // path of the file on the device.
+    private static final String ZIP_COVERAGE_FILES_COMMAND =
+            String.format(
+                    "find %s -name '*.gcda' | tar -cvf %s -T -",
+                    NATIVE_COVERAGE_DEVICE_PATH, COVERAGE_TAR_PATH);
+
+    // Deletes .gcda files in /data/misc/trace.
+    private static final String DELETE_COVERAGE_FILES_COMMAND =
+            String.format("find %s -name '*.gcda' -delete", NATIVE_COVERAGE_DEVICE_PATH);
+
+    private NativeCodeCoverageFlusher mFlusher;
+    private boolean mCollectCoverageOnTestEnd = true;
+    private IConfiguration mConfiguration;
+
+    @Override
+    public ITestInvocationListener init(
+            IInvocationContext context, ITestInvocationListener listener) {
+        super.init(context, listener);
+
+        if (isGcovCoverageEnabled()) {
+            // Clear coverage measurements on the device.
+            try (AdbRootElevator adbRoot = new AdbRootElevator(getDevices().get(0))) {
+                getCoverageFlusher().resetCoverage();
+            } catch (DeviceNotAvailableException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public void setConfiguration(IConfiguration config) {
+        mConfiguration = config;
+    }
+
+    private boolean isGcovCoverageEnabled() {
+        return mConfiguration != null
+                && mConfiguration.getCoverageOptions().isCoverageEnabled()
+                && mConfiguration.getCoverageOptions().getCoverageToolchains().contains(GCOV);
+    }
+
+    private NativeCodeCoverageFlusher getCoverageFlusher() {
+        if (mFlusher == null) {
+            mFlusher =
+                    new NativeCodeCoverageFlusher(
+                            getDevices().get(0),
+                            mConfiguration.getCoverageOptions().getCoverageProcesses());
+        }
+        return mFlusher;
+    }
+
+    /**
+     * Sets whether to collect coverage on testRunEnded.
+     *
+     * <p>Set this to false during re-runs, otherwise each individual test re-run will collect
+     * coverage rather than having a single merged coverage result.
+     */
+    public void setCollectOnTestEnd(boolean collect) {
+        mCollectCoverageOnTestEnd = collect;
+    }
+
+    @Override
+    public void onTestRunEnd(DeviceMetricData runData, final Map<String, Metric> runMetrics) {
+        if (!isGcovCoverageEnabled()) {
+            return;
+        }
+
+        if (mCollectCoverageOnTestEnd) {
+            logCoverageMeasurements(getRunName());
+        }
+    }
+
+    /** Pulls native coverage measurements from the device and logs them. */
+    public void logCoverageMeasurements(String runName) {
+        File coverageTar = null;
+        File coverageZip = null;
+        ITestDevice device = getRealDevices().get(0);
+
+        // Enable abd root on the device, otherwise the following commands will fail.
+        try (AdbRootElevator adbRoot = new AdbRootElevator(device)) {
+            // Flush cross-process coverage.
+            if (mConfiguration.getCoverageOptions().isCoverageFlushEnabled()) {
+                getCoverageFlusher().forceCoverageFlush();
+            }
+
+            // Compress coverage measurements on the device before pulling.
+            device.executeShellCommand(ZIP_COVERAGE_FILES_COMMAND);
+            coverageTar = device.pullFile(COVERAGE_TAR_PATH);
+            verifyNotNull(
+                    coverageTar,
+                    "Failed to pull the native code coverage file %s",
+                    COVERAGE_TAR_PATH);
+            device.deleteFile(COVERAGE_TAR_PATH);
+
+            coverageZip = convertTarToZip(coverageTar);
+
+            try (FileInputStreamSource source = new FileInputStreamSource(coverageZip, true)) {
+                testLog(runName + "_native_runtime_coverage", LogDataType.NATIVE_COVERAGE, source);
+            }
+
+            // Delete coverage files on the device.
+            device.executeShellCommand(DELETE_COVERAGE_FILES_COMMAND);
+        } catch (DeviceNotAvailableException | IOException e) {
+            throw new RuntimeException(e);
+        } finally {
+            FileUtil.deleteFile(coverageTar);
+            FileUtil.deleteFile(coverageZip);
+        }
+    }
+
+    /**
+     * Converts a .tar file to a .zip file.
+     *
+     * @param tar the .tar file to convert
+     * @return a .zip file with the same contents
+     * @throws IOException
+     */
+    private File convertTarToZip(File tar) throws IOException {
+        File untarDir = null;
+        try {
+            untarDir = FileUtil.createTempDir("gcov_coverage");
+            TarUtil.unTar(tar, untarDir);
+            return ZipUtil.createZip(Arrays.asList(untarDir.listFiles()), "native_coverage");
+        } finally {
+            FileUtil.recursiveDelete(untarDir);
+        }
+    }
+}
diff --git a/src/com/android/tradefed/device/metric/IncidentReportCollector.java b/src/com/android/tradefed/device/metric/IncidentReportCollector.java
index f6246fc..a225e86 100644
--- a/src/com/android/tradefed/device/metric/IncidentReportCollector.java
+++ b/src/com/android/tradefed/device/metric/IncidentReportCollector.java
@@ -18,19 +18,30 @@
 
 import android.os.IncidentProto;
 
+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.log.LogUtil.CLog;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ByteArrayInputStreamSource;
+import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.InputStreamSource;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
 import com.google.protobuf.InvalidProtocolBufferException;
 
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.util.Map;
 
 /**
- * Pulls and processes incident reports that are reported device-side.
- *
- * <p>TODO(b/119418529): Collect an incident report host-side on failure.
+ * Pulls and processes incident reports that are reported device-side and collects incident reports
+ * host-side at the end of a test run if configured to do so.
  */
 @OptionClass(alias = "incident-collector")
 public class IncidentReportCollector extends FilePullerLogCollector {
@@ -38,13 +49,68 @@
     private static final String INCIDENT_KEY_MATCHER = "incident-report";
     // Suffix for all of the logs that are processed incident reports.
     private static final String PROCESSED_KEY_SUFFIX = "-processed";
+    // Incident report command and associated timeouts that are used.
+    static final String INCIDENT_REPORT_CMD = "incident -b -p EXPLICIT";
+    private static final long INCIDENT_DUMP_TIMEOUT = 5 * 60 * 1000;
+    private static final long DEVICE_AVAILABLE_TIMEOUT = 10 * 60 * 1000;
+
+    @Option(
+            name = "incident-on-test-run-end",
+            description =
+                    "Collect an incident report at the end of each test run. This report will not"
+                            + " be collected if a report was collected on-device and logged prior.")
+    private boolean mIncidentOnRunEnd = false;
 
     public IncidentReportCollector() {
         addKeys(INCIDENT_KEY_MATCHER);
     }
 
     @Override
+    public void onTestRunEnd(
+            DeviceMetricData runData, final Map<String, Metric> currentRunMetrics) {
+        super.onTestRunEnd(runData, currentRunMetrics);
+        // Only collect if set and there wasn't an on-device report collected.
+        if (!mIncidentOnRunEnd) {
+            return;
+        }
+        for (ITestDevice device : getDevices()) {
+            File outFile = null;
+            try {
+                // Ensure the device is available for the command.
+                device.waitForDeviceAvailable(DEVICE_AVAILABLE_TIMEOUT);
+                // Collect the incident report to a new file.
+                outFile = File.createTempFile("incident-on-test-run-end", ".pb");
+                FileOutputStream outStream = new FileOutputStream(outFile, false);
+                CommandResult result = device.executeShellV2Command(INCIDENT_REPORT_CMD, outStream);
+                // Complain (and say why) if something didn't go right.
+                if (result.getStatus().equals(CommandStatus.SUCCESS)) {
+                    // Log the extra file and process it as a report.
+                    try (InputStreamSource source = new FileInputStreamSource(outFile)) {
+                        testLog(outFile.getName(), LogDataType.PB, source);
+                    }
+                    logProcessedReport(outFile);
+                } else {
+                    CLog.e(
+                            "There was an error collecting an incident report: %s",
+                            result.getStderr());
+                }
+                // Just wrap exceptions and print them.
+            } catch (DeviceNotAvailableException dnae) {
+                CLog.e(dnae);
+            } catch (IOException ioe) {
+                CLog.e(ioe);
+            } finally {
+                FileUtil.deleteFile(outFile);
+            }
+        }
+    }
+
+    @Override
     protected void postProcessMetricFile(String key, File metricFile, DeviceMetricData runData) {
+        logProcessedReport(metricFile);
+    }
+
+    private void logProcessedReport(File metricFile) {
         // Read and interpret the incident report's bytes.
         IncidentProto processedReport;
         try {
diff --git a/src/com/android/tradefed/device/metric/JavaCodeCoverageCollector.java b/src/com/android/tradefed/device/metric/JavaCodeCoverageCollector.java
new file mode 100644
index 0000000..d04b1ca
--- /dev/null
+++ b/src/com/android/tradefed/device/metric/JavaCodeCoverageCollector.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.device.metric;
+
+import static com.google.common.base.Verify.verifyNotNull;
+import static com.google.common.io.Files.getNameWithoutExtension;
+
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.IConfigurationReceiver;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.logger.CurrentInvocation;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.FailureDescription;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
+import com.android.tradefed.testtype.coverage.CoverageOptions;
+import com.android.tradefed.util.AdbRootElevator;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.JavaCodeCoverageFlusher;
+import com.android.tradefed.util.ProcessInfo;
+import com.android.tradefed.util.PsParser;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+
+import org.jacoco.core.tools.ExecFileLoader;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.List;
+
+/**
+ * A {@link com.android.tradefed.device.metric.BaseDeviceMetricCollector} that will pull Java
+ * coverage measurements off of the device and log them as test artifacts.
+ */
+public final class JavaCodeCoverageCollector extends BaseDeviceMetricCollector
+        implements IConfigurationReceiver {
+
+    public static final String MERGE_COVERAGE_MEASUREMENTS_TEST_NAME = "mergeCoverageMeasurements";
+    public static final String COVERAGE_MEASUREMENT_KEY = "coverageFilePath";
+    public static final String COVERAGE_DIRECTORY = "/data/misc/trace";
+    public static final String FIND_COVERAGE_FILES =
+            String.format("find %s -name '*.ec'", COVERAGE_DIRECTORY);
+
+    @Option(
+            name = "merge-coverage-measurements",
+            description =
+                    "Merge coverage measurements after all tests are complete rather than logging individual measurements.")
+    private boolean mMergeCoverageMeasurements = false;
+
+    private final ExecFileLoader mExecFileLoader = new ExecFileLoader();
+
+    private JavaCodeCoverageFlusher mFlusher;
+    private IConfiguration mConfiguration;
+
+    @Override
+    public ITestInvocationListener init(
+            IInvocationContext context, ITestInvocationListener listener) {
+        super.init(context, listener);
+
+        if (isJavaCoverageEnabled()) {
+            try (AdbRootElevator adbRoot = new AdbRootElevator(getDevices().get(0))) {
+                getCoverageFlusher().resetCoverage();
+            } catch (DeviceNotAvailableException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public void setConfiguration(IConfiguration configuration) {
+        mConfiguration = configuration;
+    }
+
+    private JavaCodeCoverageFlusher getCoverageFlusher() {
+        if (mFlusher == null) {
+            mFlusher =
+                    new JavaCodeCoverageFlusher(
+                            getRealDevices().get(0),
+                            mConfiguration.getCoverageOptions().getCoverageProcesses());
+        }
+        return mFlusher;
+    }
+
+    @VisibleForTesting
+    public void setCoverageFlusher(JavaCodeCoverageFlusher flusher) {
+        mFlusher = flusher;
+    }
+
+    @VisibleForTesting
+    public void setMergeMeasurements(boolean merge) {
+        mMergeCoverageMeasurements = merge;
+    }
+
+    @Override
+    public void onTestRunEnd(DeviceMetricData runData, final Map<String, Metric> runMetrics) {
+        if (!isJavaCoverageEnabled()) {
+            return;
+        }
+        if (MERGE_COVERAGE_MEASUREMENTS_TEST_NAME.equals(getRunName())) {
+            // Log the merged runtime coverage measurement.
+            try {
+                File mergedMeasurements =
+                        FileUtil.createTempFile(
+                                "merged_runtime_coverage_",
+                                "." + LogDataType.COVERAGE.getFileExt());
+
+                mExecFileLoader.save(mergedMeasurements, false);
+
+                // Save the merged measurement as a test log.
+                logCoverageMeasurement("merged_runtime_coverage", mergedMeasurements);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        } else {
+            // Get the path of the coverage measurement on the device.
+            Metric devicePathMetric = runMetrics.get(COVERAGE_MEASUREMENT_KEY);
+            if (devicePathMetric == null) {
+                super.testRunFailed(
+                        createCodeCoverageFailure("No Java code coverage measurement."));
+                return;
+            }
+            String testCoveragePath = devicePathMetric.getMeasurements().getSingleString();
+            if (testCoveragePath == null) {
+                super.testRunFailed(
+                        createCodeCoverageFailure("No Java code coverage measurement."));
+                return;
+            }
+
+            ITestDevice device = getRealDevices().get(0);
+            ImmutableList.Builder<String> devicePaths = ImmutableList.builder();
+            devicePaths.add(testCoveragePath);
+
+            try (AdbRootElevator adbRoot = new AdbRootElevator(device)) {
+                if (mConfiguration.getCoverageOptions().isCoverageFlushEnabled()) {
+                    getCoverageFlusher().forceCoverageFlush();
+                }
+
+                // Find all .ec files in /data/misc/trace and pull them from the device as well.
+                String fileList = device.executeShellCommand(FIND_COVERAGE_FILES);
+                devicePaths.addAll(Splitter.on('\n').omitEmptyStrings().split(fileList));
+
+                collectAndLogCoverageMeasurements(device, devicePaths.build());
+            } catch (DeviceNotAvailableException | IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    private void logCoverageMeasurement(String name, File coverageFile) {
+        try (FileInputStreamSource source = new FileInputStreamSource(coverageFile, true)) {
+            testLog(name, LogDataType.COVERAGE, source);
+        }
+    }
+
+    private void collectAndLogCoverageMeasurements(ITestDevice device, List<String> devicePaths)
+            throws IOException, DeviceNotAvailableException {
+        List<Integer> activePids = getRunningProcessIds(device);
+
+        for (String devicePath : devicePaths) {
+            File coverageFile = device.pullFile(devicePath);
+
+            if (devicePath.endsWith(".mm.ec")) {
+                // Check if the process was still running. The file will have the format
+                // /data/misc/trace/jacoco-XXXXX.mm.ec where XXXXX is the process id.
+                int start = devicePath.indexOf('-') + 1;
+                int end = devicePath.indexOf('.');
+                int pid = Integer.parseInt(devicePath.substring(start, end));
+                if (!activePids.contains(pid)) {
+                    device.deleteFile(devicePath);
+                }
+            } else {
+                device.deleteFile(devicePath);
+            }
+
+            verifyNotNull(
+                    coverageFile, "Failed to pull the Java code coverage file from %s", devicePath);
+
+            // When merging, load the measurement data. Otherwise log the measurement
+            // immediately.
+            try {
+                if (mMergeCoverageMeasurements) {
+                    mExecFileLoader.load(coverageFile);
+                } else {
+                    logCoverageMeasurement(
+                            getRunName()
+                                    + "_"
+                                    + getNameWithoutExtension(devicePath)
+                                    + "_runtime_coverage",
+                            coverageFile);
+                }
+            } finally {
+                FileUtil.deleteFile(coverageFile);
+            }
+        }
+    }
+
+    private List<Integer> getRunningProcessIds(ITestDevice device)
+            throws DeviceNotAvailableException {
+        List<ProcessInfo> processes = PsParser.getProcesses(device.executeShellCommand("ps -e"));
+        List<Integer> pids = new ArrayList<>();
+
+        for (ProcessInfo process : processes) {
+            pids.add(process.getPid());
+        }
+        return pids;
+    }
+
+    private FailureDescription createCodeCoverageFailure(String message) {
+        return CurrentInvocation.createFailure(message, InfraErrorIdentifier.CODE_COVERAGE_ERROR);
+    }
+
+    private boolean isJavaCoverageEnabled() {
+        return mConfiguration != null
+                && mConfiguration.getCoverageOptions().isCoverageEnabled()
+                && mConfiguration
+                        .getCoverageOptions()
+                        .getCoverageToolchains()
+                        .contains(CoverageOptions.Toolchain.JACOCO);
+    }
+}
diff --git a/src/com/android/tradefed/invoker/DelegatedInvocationExecution.java b/src/com/android/tradefed/invoker/DelegatedInvocationExecution.java
index c4c9697..56292d2 100644
--- a/src/com/android/tradefed/invoker/DelegatedInvocationExecution.java
+++ b/src/com/android/tradefed/invoker/DelegatedInvocationExecution.java
@@ -24,6 +24,7 @@
 import com.android.tradefed.error.HarnessRuntimeException;
 import com.android.tradefed.invoker.TestInvocation.Stage;
 import com.android.tradefed.log.ITestLogger;
+import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogDataType;
@@ -131,6 +132,7 @@
             IRunUtil runUtil = createRunUtil(receiver.getSocketServerPort());
             CommandResult result = null;
             RuntimeException runtimeException = null;
+            CLog.d("Command line: %s", commandLine);
             try {
                 result =
                         runUtil.runTimedCmd(
@@ -159,7 +161,7 @@
             }
             if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
                 throw new HarnessRuntimeException(
-                        "Delegated invocation timed out.", InfraErrorIdentifier.UNDETERMINED);
+                        "Delegated invocation timed out.", InfraErrorIdentifier.INVOCATION_TIMEOUT);
             }
         } finally {
             StreamUtil.close(mStderr);
diff --git a/src/com/android/tradefed/invoker/InvocationExecution.java b/src/com/android/tradefed/invoker/InvocationExecution.java
index 8e522a9..7a4ae8a 100644
--- a/src/com/android/tradefed/invoker/InvocationExecution.java
+++ b/src/com/android/tradefed/invoker/InvocationExecution.java
@@ -27,6 +27,7 @@
 import com.android.tradefed.build.IDeviceBuildProvider;
 import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.IConfigurationReceiver;
 import com.android.tradefed.config.IDeviceConfiguration;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
@@ -82,6 +83,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -230,11 +232,12 @@
 
             mTrackTargetPreparers = new ConcurrentHashMap<>();
             int index = 0;
-            if (config.getCommandOptions().shouldUseReplicateSetup()
+            if ((config.getCommandOptions().shouldUseParallelSetup()
+                            || config.getCommandOptions().shouldUseReplicateSetup())
                     && config.getDeviceConfig().size() > 1) {
-                CLog.d("Using parallel setup due to replicated setup enabled.");
+                CLog.d("Using parallel setup.");
                 ParallelDeviceExecutor<Boolean> executor =
-                        new ParallelDeviceExecutor<>(testInfo.getContext().getDevices());
+                        new ParallelDeviceExecutor<>(testInfo.getContext().getDevices().size());
                 List<Callable<Boolean>> callableTasks = new ArrayList<>();
                 for (String deviceName : testInfo.getContext().getDeviceConfigNames()) {
                     mTrackTargetPreparers.put(deviceName, new HashSet<>());
@@ -251,8 +254,8 @@
                     callableTasks.add(callableTask);
                     index++;
                 }
-                // Run setup with 30 minutes right now.
-                executor.invokeAll(callableTasks, 30, TimeUnit.MINUTES);
+                Duration timeout = config.getCommandOptions().getParallelSetupTimeout();
+                executor.invokeAll(callableTasks, timeout.toMillis(), TimeUnit.MILLISECONDS);
                 if (executor.hasErrors()) {
                     List<Throwable> errors = executor.getErrors();
                     // TODO: Handle throwing multi-exceptions, right now throw the first one.
@@ -745,6 +748,9 @@
                 if (collector.isDisabled()) {
                     CLog.d("%s has been disabled. Skipping.", collector);
                 } else {
+                    if (collector instanceof IConfigurationReceiver) {
+                        ((IConfigurationReceiver) collector).setConfiguration(config);
+                    }
                     listenerWithCollectors =
                             collector.init(info.getContext(), listenerWithCollectors);
                     TfObjectTracker.countWithParents(collector.getClass());
diff --git a/src/com/android/tradefed/invoker/TestInvocation.java b/src/com/android/tradefed/invoker/TestInvocation.java
index 2927f19..c4ffc43 100644
--- a/src/com/android/tradefed/invoker/TestInvocation.java
+++ b/src/com/android/tradefed/invoker/TestInvocation.java
@@ -35,6 +35,7 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.ITestDevice.RecoveryMode;
 import com.android.tradefed.device.NativeDevice;
+import com.android.tradefed.device.RemoteAndroidDevice;
 import com.android.tradefed.device.StubDevice;
 import com.android.tradefed.device.TcpDevice;
 import com.android.tradefed.device.TestDeviceState;
@@ -42,6 +43,7 @@
 import com.android.tradefed.device.cloud.NestedRemoteDevice;
 import com.android.tradefed.device.cloud.RemoteAndroidVirtualDevice;
 import com.android.tradefed.error.HarnessException;
+import com.android.tradefed.error.HarnessRuntimeException;
 import com.android.tradefed.error.IHarnessException;
 import com.android.tradefed.guice.InvocationScope;
 import com.android.tradefed.invoker.logger.CurrentInvocation;
@@ -163,6 +165,7 @@
     private Long mStopRequestTime = null;
     private boolean mTestStarted = false;
     private boolean mInvocationFailed = false;
+    private boolean mDelegatedInvocation = false;
     private List<IScheduledInvocationListener> mSchedulerListeners = new ArrayList<>();
 
     /**
@@ -213,7 +216,6 @@
             throws Throwable {
         ReportHostLog reportThread = new ReportHostLog(listener, config);
         Runtime.getRuntime().addShutdownHook(reportThread);
-        boolean resumed = false;
         String bugreportName = null;
         long startTime = System.currentTimeMillis();
         long elapsedTime = -1;
@@ -305,37 +307,39 @@
             }
             CurrentInvocation.setActionInProgress(ActionInProgress.TEAR_DOWN);
             getRunUtil().allowInterrupt(false);
-            if (config.getCommandOptions().takeBugreportOnInvocationEnded() ||
-                    config.getCommandOptions().takeBugreportzOnInvocationEnded()) {
-                if (bugreportName != null) {
-                    CLog.i("Bugreport to be taken for failure instead of invocation ended.");
-                } else {
-                    bugreportName = INVOCATION_ENDED_BUGREPORT_NAME;
+            if (!mDelegatedInvocation) {
+                if (config.getCommandOptions().takeBugreportOnInvocationEnded()
+                        || config.getCommandOptions().takeBugreportzOnInvocationEnded()) {
+                    if (bugreportName != null) {
+                        CLog.i("Bugreport to be taken for failure instead of invocation ended.");
+                    } else {
+                        bugreportName = INVOCATION_ENDED_BUGREPORT_NAME;
+                    }
                 }
-            }
-            if (bugreportName != null) {
-                if (context.getDevices().size() == 1 || badDevice != null) {
-                    ITestDevice collectBugreport = badDevice;
-                    if (collectBugreport == null) {
-                        collectBugreport = context.getDevices().get(0);
+                if (bugreportName != null) {
+                    if (context.getDevices().size() == 1 || badDevice != null) {
+                        ITestDevice collectBugreport = badDevice;
+                        if (collectBugreport == null) {
+                            collectBugreport = context.getDevices().get(0);
+                        }
+                        // If we have identified a faulty device only take the bugreport on it.
+                        takeBugreport(collectBugreport, listener, bugreportName);
+                    } else if (context.getDevices().size() > 1) {
+                        ParallelDeviceExecutor<Boolean> executor =
+                                new ParallelDeviceExecutor<>(context.getDevices().size());
+                        List<Callable<Boolean>> callableTasks = new ArrayList<>();
+                        final String reportName = bugreportName;
+                        for (ITestDevice device : context.getDevices()) {
+                            Callable<Boolean> callableTask =
+                                    () -> {
+                                        takeBugreport(device, listener, reportName);
+                                        return true;
+                                    };
+                            callableTasks.add(callableTask);
+                        }
+                        // Capture the bugreports best effort, ignore the results.
+                        executor.invokeAll(callableTasks, 5, TimeUnit.MINUTES);
                     }
-                    // If we have identified a faulty device only take the bugreport on it.
-                    takeBugreport(collectBugreport, listener, bugreportName);
-                } else if (context.getDevices().size() > 1) {
-                    ParallelDeviceExecutor<Boolean> executor =
-                            new ParallelDeviceExecutor<>(context.getDevices());
-                    List<Callable<Boolean>> callableTasks = new ArrayList<>();
-                    final String reportName = bugreportName;
-                    for (ITestDevice device : context.getDevices()) {
-                        Callable<Boolean> callableTask =
-                                () -> {
-                                    takeBugreport(device, listener, reportName);
-                                    return true;
-                                };
-                        callableTasks.add(callableTask);
-                    }
-                    // Capture the bugreports best effort, ignore the results.
-                    executor.invokeAll(callableTasks, 5, TimeUnit.MINUTES);
                 }
             }
             // Save the device executeShellCommand logs
@@ -383,6 +387,10 @@
                                     mStopCause);
                     FailureDescription failure =
                             FailureDescription.create(message, FailureStatus.CANCELLED);
+                    failure.setErrorIdentifier(InfraErrorIdentifier.INVOCATION_CANCELLED);
+                    failure.setCause(
+                            new HarnessRuntimeException(
+                                    message, InfraErrorIdentifier.INVOCATION_CANCELLED));
                     reportFailure(failure, listener);
                     PrettyPrintDelimiter.printStageDelimiter(message);
                     if (mStopRequestTime != null) {
@@ -398,22 +406,7 @@
                 Runtime.getRuntime().removeShutdownHook(reportThread);
 
                 elapsedTime = System.currentTimeMillis() - startTime;
-                if (!resumed) {
-                    // Init a log for the end of the host_log.
-                    ILeveledLogOutput endHostLog = config.getLogOutput();
-                    endHostLog.init();
-                    getLogRegistry().registerLogger(endHostLog);
-                    PrettyPrintDelimiter.printStageDelimiter("===== Result Reporters =====");
-                    try {
-                        // Copy the invocation metrics to the context
-                        ((InvocationContext) context).logInvocationMetrics();
-                        listener.invocationEnded(elapsedTime);
-                    } finally {
-                        InvocationMetricLogger.clearInvocationMetrics();
-                        endHostLog.closeLog();
-                        getLogRegistry().unregisterLogger();
-                    }
-                }
+                reportInvocationEnded(config, context, listener, elapsedTime);
             } finally {
                 TfObjectTracker.clearTracking();
                 CurrentInvocation.clearInvocationInfos();
@@ -497,7 +490,7 @@
 
     private void reportHostLog(ITestInvocationListener listener, IConfiguration config) {
         String name = TRADEFED_LOG_NAME;
-        if (config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT) != null) {
+        if (mDelegatedInvocation) {
             name = TRADEFED_DELEGATED_LOG_NAME;
         }
         reportHostLog(listener, config, name);
@@ -649,7 +642,7 @@
             invocationPath.reportLogs(device, listener, Stage.ERROR);
         }
         reportHostLog(listener, config);
-        listener.invocationEnded(0L);
+        reportInvocationEnded(config, testInfo.getContext(), listener, 0L);
         return false;
     }
 
@@ -702,7 +695,7 @@
                 invocationPath.reportLogs(device, listener, Stage.ERROR);
             }
             reportHostLog(listener, config);
-            listener.invocationEnded(0L);
+            reportInvocationEnded(config, context, listener, 0L);
             return false;
         }
     }
@@ -796,6 +789,7 @@
             mode = RunMode.REMOTE_INVOCATION;
         }
         if (config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT) != null) {
+            mDelegatedInvocation = true;
             mode = RunMode.DELEGATED_INVOCATION;
         }
         IInvocationExecution invocationPath = createInvocationExec(mode);
@@ -1128,7 +1122,8 @@
         int countVirtualLost = 0;
         for (Entry<ITestDevice, FreeDeviceState> fds : devicesStates.entrySet()) {
             // TODO: Rely on the FailureStatus for lost devices instead
-            if (fds.getKey().getIDevice() instanceof TcpDevice
+            if ((fds.getKey().getIDevice() instanceof TcpDevice
+                            || fds.getKey() instanceof RemoteAndroidDevice)
                     && exception instanceof DeviceNotAvailableException) {
                 countVirtualLost++;
                 continue;
@@ -1154,6 +1149,35 @@
         return devicesStates;
     }
 
+    private void reportInvocationEnded(
+            IConfiguration config,
+            IInvocationContext context,
+            ITestInvocationListener listener,
+            long elapsedTime) {
+        // Init a log for the end of the host_log.
+        ILeveledLogOutput endHostLog = config.getLogOutput();
+        try {
+            endHostLog.init();
+            getLogRegistry().registerLogger(endHostLog);
+        } catch (IOException e) {
+            CLog.e(e);
+            endHostLog = null;
+        }
+
+        PrettyPrintDelimiter.printStageDelimiter("===== Result Reporters =====");
+        try {
+            // Copy the invocation metrics to the context
+            ((InvocationContext) context).logInvocationMetrics();
+            listener.invocationEnded(elapsedTime);
+        } finally {
+            InvocationMetricLogger.clearInvocationMetrics();
+            if (endHostLog != null) {
+                endHostLog.closeLog();
+                getLogRegistry().unregisterLogger();
+            }
+        }
+    }
+
     /** Helper Thread that ensures host_log is reported in case of killed JVM */
     private class ReportHostLog extends Thread {
 
diff --git a/src/com/android/tradefed/invoker/shard/StrictShardHelper.java b/src/com/android/tradefed/invoker/shard/StrictShardHelper.java
index f3aeffc..cf30b05 100644
--- a/src/com/android/tradefed/invoker/shard/StrictShardHelper.java
+++ b/src/com/android/tradefed/invoker/shard/StrictShardHelper.java
@@ -35,7 +35,10 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /** Sharding strategy to create strict shards that do not report together, */
 public class StrictShardHelper extends ShardHelper {
@@ -49,6 +52,7 @@
             ITestLogger logger) {
         Integer shardCount = config.getCommandOptions().getShardCount();
         Integer shardIndex = config.getCommandOptions().getShardIndex();
+        boolean optimizeMainline = config.getCommandOptions().getOptimizeMainlineTest();
 
         if (shardIndex == null) {
             return super.shardConfig(config, testInfo, rescheduler, logger);
@@ -69,11 +73,50 @@
             splitList = splitTests(listAllTests, shardCount).get(shardIndex);
         }
         aggregateSuiteModules(splitList);
+        if (optimizeMainline) {
+            CLog.i("Reordering the test modules list for index: %s", shardIndex);
+            reorderTestModules(splitList);
+        }
         config.setTests(splitList);
         return false;
     }
 
     /**
+     * Helper to re order the list full list of {@link IRemoteTest} for mainline.
+     *
+     * @param tests the {@link IRemoteTest} containing all the tests that need to run.
+     */
+    private void reorderTestModules(List<IRemoteTest> tests) {
+        Collections.sort(tests, new Comparator<IRemoteTest>() {
+            @Override
+            public int compare(IRemoteTest o1, IRemoteTest o2) {
+                String moduleId1 = ((ITestSuite)o1).getDirectModule().getId();
+                String moduleId2 = ((ITestSuite)o2).getDirectModule().getId();
+                return getMainlineId(moduleId1).compareTo(getMainlineId(moduleId2));
+            }
+        });
+    }
+
+    /**
+     * Returns the parameterized mainline modules' name defined in the square brackets.
+     *
+     * @param id The module's name.
+     * @throws RuntimeException if the module name doesn't match the pattern for mainline modules.
+     */
+    private String getMainlineId(String id) {
+        // Pattern used to identify the parameterized mainline modules defined in the square
+        // brackets.
+        Pattern parameterizedMainlineRegex = Pattern.compile("\\[(.*(\\.apk|.apex|.apks))\\]$");
+        Matcher m = parameterizedMainlineRegex.matcher(id);
+        if (m.find()) {
+            return m.group(1);
+        }
+        throw new RuntimeException(
+                String.format("Module: %s doesn't match the pattern for mainline modules. The " +
+                        "pattern should end with apk/apex/apks.", id));
+    }
+
+    /**
      * Helper to return the full list of {@link IRemoteTest} based on {@link IShardableTest} split.
      *
      * @param config the {@link IConfiguration} describing the invocation.
diff --git a/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java b/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java
index 666f19c..9efc9a6 100644
--- a/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java
+++ b/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java
@@ -178,6 +178,9 @@
         try {
             ITestInvocationListener listenerWithCollectors = listener;
             for (IMetricCollector collector : mCollectors) {
+                if (collector instanceof IConfigurationReceiver) {
+                    ((IConfigurationReceiver) collector).setConfiguration(mConfig);
+                }
                 listenerWithCollectors = collector.init(info.getContext(), listenerWithCollectors);
             }
             while (true) {
diff --git a/src/com/android/tradefed/monitoring/LabResourceDeviceMonitor.java b/src/com/android/tradefed/monitoring/LabResourceDeviceMonitor.java
new file mode 100644
index 0000000..8366e0e
--- /dev/null
+++ b/src/com/android/tradefed/monitoring/LabResourceDeviceMonitor.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.monitoring;
+
+import com.android.loganalysis.util.config.OptionClass;
+import com.android.tradefed.device.DeviceAllocationState;
+import com.android.tradefed.device.IDeviceMonitor;
+import com.android.tradefed.log.LogUtil;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.dualhomelab.monitoringagent.resourcemonitoring.LabResource;
+import com.google.dualhomelab.monitoringagent.resourcemonitoring.LabResourceRequest;
+import com.google.dualhomelab.monitoringagent.resourcemonitoring.LabResourceServiceGrpc;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Optional;
+import java.util.concurrent.Executors;
+
+import io.grpc.Server;
+import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
+import io.grpc.stub.StreamObserver;
+
+/** The lab resource monitor which initializes/manages the gRPC server for LabResourceService. */
+@OptionClass(alias = "lab-resource-monitor")
+public class LabResourceDeviceMonitor extends LabResourceServiceGrpc.LabResourceServiceImplBase
+        implements IDeviceMonitor {
+    public static final String SERVER_HOSTNAME = "localhost";
+    public static final int DEFAULT_PORT = 8887;
+    public static final int DEFAULT_THREAD_COUNT = 1;
+    private Optional<Server> mServer = Optional.empty();
+
+    @VisibleForTesting
+    Optional<Server> getServer() {
+        return mServer;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void run() {
+        if (!mServer.isPresent()) {
+            mServer =
+                    Optional.of(
+                            NettyServerBuilder.forAddress(
+                                            new InetSocketAddress(SERVER_HOSTNAME, DEFAULT_PORT))
+                                    .addService(this)
+                                    .executor(Executors.newFixedThreadPool(DEFAULT_THREAD_COUNT))
+                                    .build());
+            try {
+                mServer.get().start();
+            } catch (IOException e) {
+                LogUtil.CLog.e(e);
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void stop() {
+        mServer.ifPresent(Server::shutdown);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setDeviceLister(DeviceLister lister) {
+        // Ignore
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void notifyDeviceStateChange(
+            String serial, DeviceAllocationState oldState, DeviceAllocationState newState) {
+        // Ignore
+    }
+
+    /** The gRPC request handler. */
+    @Override
+    public void getLabResource(
+            LabResourceRequest request, StreamObserver<LabResource> responseObserver) {
+        super.getLabResource(request, responseObserver);
+    }
+}
diff --git a/src/com/android/tradefed/postprocessor/StatsdEventMetricPostProcessor.java b/src/com/android/tradefed/postprocessor/StatsdEventMetricPostProcessor.java
index 8833ed5..4a934d8 100644
--- a/src/com/android/tradefed/postprocessor/StatsdEventMetricPostProcessor.java
+++ b/src/com/android/tradefed/postprocessor/StatsdEventMetricPostProcessor.java
@@ -24,6 +24,7 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.util.MultiMap;
+import com.android.tradefed.util.ProtoUtil;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
 import com.google.protobuf.Descriptors.FieldDescriptor;
@@ -148,48 +149,6 @@
         return metrics;
     }
 
-    /**
-     * Get a nested field reference, i.e. field_1.field_2.field_3, from a proto message as a string.
-     * Returns an empty list when a field cannot be found, either because it's invalid or does not
-     * exist in the message.
-     *
-     * <p>If the field reference contains repeated fields, each instance is expanded, resulting in a
-     * list of strings.
-     */
-    private List<String> getNestedFieldFromMessageAsStrings(
-            Object messageOrObject, List<String> references) {
-        if (references.isEmpty()) {
-            return Arrays.asList(String.valueOf(messageOrObject));
-        }
-        if (!(messageOrObject instanceof Message)) {
-            CLog.e(
-                    "Attempting to read field %s from object of type %s, "
-                            + "which is not a proto message.",
-                    references.get(0), messageOrObject.getClass());
-            return new ArrayList<String>();
-        }
-        Message message = (Message) messageOrObject;
-        String reference = references.get(0);
-        FieldDescriptor fieldDescriptor = message.getDescriptorForType().findFieldByName(reference);
-        if (fieldDescriptor == null) {
-            CLog.e("Could not find field %s in message %s.", reference, message);
-            return new ArrayList<String>();
-        }
-        Object fieldValue = message.getField(fieldDescriptor);
-        if (fieldValue instanceof List) {
-            return ((List<? extends Object>) fieldValue)
-                    .stream()
-                    .flatMap(
-                            v ->
-                                    getNestedFieldFromMessageAsStrings(
-                                                    v, references.subList(1, references.size()))
-                                            .stream())
-                    .collect(Collectors.toList());
-        }
-        return getNestedFieldFromMessageAsStrings(
-                fieldValue, references.subList(1, references.size()));
-    }
-
     /** Fill in the placeholders in the formatter using the proto message as source. */
     private List<String> fillInPlaceholders(
             String formatter, EventMetricData eventMetric, Message atomContent) {
@@ -202,12 +161,12 @@
             List<String> actual = new ArrayList();
             if (fieldReference.startsWith("_")) {
                 actual.addAll(
-                        getNestedFieldFromMessageAsStrings(
+                        ProtoUtil.getNestedFieldFromMessageAsStrings(
                                 eventMetric,
                                 Arrays.asList(fieldReference.substring(1).split("\\."))));
             } else {
                 actual.addAll(
-                        getNestedFieldFromMessageAsStrings(
+                        ProtoUtil.getNestedFieldFromMessageAsStrings(
                                 atomContent, Arrays.asList(fieldReference.split("\\."))));
             }
             // If both the existing expansion results and newly expanded results have multiple
diff --git a/src/com/android/tradefed/result/CollectingTestListener.java b/src/com/android/tradefed/result/CollectingTestListener.java
index d9ec6ec..a51d2ae 100644
--- a/src/com/android/tradefed/result/CollectingTestListener.java
+++ b/src/com/android/tradefed/result/CollectingTestListener.java
@@ -311,6 +311,12 @@
     }
 
     @Override
+    public void testAssumptionFailure(TestDescription test, FailureDescription failure) {
+        setCountDirty();
+        mCurrentTestRunResult.testAssumptionFailure(test, failure);
+    }
+
+    @Override
     public void testIgnored(TestDescription test) {
         setCountDirty();
         mCurrentTestRunResult.testIgnored(test);
diff --git a/src/com/android/tradefed/result/FilteredResultForwarder.java b/src/com/android/tradefed/result/FilteredResultForwarder.java
index 6be2c63..2175fc6 100644
--- a/src/com/android/tradefed/result/FilteredResultForwarder.java
+++ b/src/com/android/tradefed/result/FilteredResultForwarder.java
@@ -22,7 +22,7 @@
 import java.util.Map;
 
 /**
- * Variant of {@link ResultForwarder} that only allows a whitelist of {@link TestDescription} to be
+ * Variant of {@link ResultForwarder} that only allows an allowlist of {@link TestDescription} to be
  * reported.
  */
 public class FilteredResultForwarder extends ResultForwarder {
diff --git a/src/com/android/tradefed/result/JsonHttpTestResultReporter.java b/src/com/android/tradefed/result/JsonHttpTestResultReporter.java
index faa3697..b89e4b0 100644
--- a/src/com/android/tradefed/result/JsonHttpTestResultReporter.java
+++ b/src/com/android/tradefed/result/JsonHttpTestResultReporter.java
@@ -214,8 +214,6 @@
                 }
                 allTestMetrics.put(reportingUnit, runResultMetrics);
                 resultsName.append(String.format("%s%s", reportingUnit, RESULT_SEPARATOR));
-            } else {
-                CLog.d("Skipping metrics for %s because results are empty.", runResult.getName());
             }
 
             // Parse test metrics
@@ -237,8 +235,6 @@
                 if (testResult.getMetrics().size() > 0) {
                     JSONObject testResultMetrics = new JSONObject(testResult.getMetrics());
                     allTestMetrics.put(reportingUnit, testResultMetrics);
-                } else {
-                    CLog.d("Skipping metrics for %s because results are empty.", testDescription);
                 }
             }
         }
diff --git a/src/com/android/tradefed/result/LogSaverResultForwarder.java b/src/com/android/tradefed/result/LogSaverResultForwarder.java
index cf11160..0083247 100644
--- a/src/com/android/tradefed/result/LogSaverResultForwarder.java
+++ b/src/com/android/tradefed/result/LogSaverResultForwarder.java
@@ -50,7 +50,12 @@
     @Override
     public void invocationStarted(IInvocationContext context) {
         // Intentionally call invocationStarted for the log saver first.
-        mLogSaver.invocationStarted(context);
+        try {
+            mLogSaver.invocationStarted(context);
+        } catch (RuntimeException e) {
+            CLog.e("Caught runtime exception from log saver: %s", mLogSaver.getClass().getName());
+            CLog.e(e);
+        }
         InvocationSummaryHelper.reportInvocationStarted(getListeners(), context);
     }
 
diff --git a/src/com/android/tradefed/result/LogcatCrashResultForwarder.java b/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
index 6da0c2d..a14cb8a 100644
--- a/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
+++ b/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
@@ -19,6 +19,8 @@
 import com.android.loganalysis.item.LogcatItem;
 import com.android.loganalysis.parser.LogcatParser;
 import com.android.tradefed.device.ITestDevice;
+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.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.error.DeviceErrorIdentifier;
@@ -41,6 +43,11 @@
     /** Special error message from the instrumentation when something goes wrong on device side. */
     public static final String ERROR_MESSAGE = "Process crashed.";
     public static final String SYSTEM_CRASH_MESSAGE = "System has crashed.";
+    public static final String TIMEOUT_MESSAGES[] = {
+        "Failed to receive adb shell test output",
+        "TimeoutException when running tests",
+        "TestTimedOutException: test timed out after",
+    };
 
     public static final int MAX_NUMBER_CRASH = 3;
 
@@ -66,16 +73,26 @@
 
     @Override
     public void testFailed(TestDescription test, String trace) {
-        // If the test case was detected as crashing the instrumentation, we add the crash to it.
-        trace = extractCrashAndAddToMessage(trace, mStartTime);
-        super.testFailed(test, trace);
+        testFailed(test, FailureDescription.create(trace));
     }
 
     @Override
     public void testFailed(TestDescription test, FailureDescription failure) {
         // If the test case was detected as crashing the instrumentation, we add the crash to it.
         String trace = extractCrashAndAddToMessage(failure.getErrorMessage(), mStartTime);
+        if (trace.compareTo(failure.getErrorMessage()) != 0) {
+            // Crash stack trace found, consider this a test failure.
+            failure.setFailureStatus(FailureStatus.TEST_FAILURE);
+        } else if (isTimeout(failure.getErrorMessage())) {
+            failure.setFailureStatus(FailureStatus.TIMED_OUT);
+        }
         failure.setErrorMessage(trace);
+        // Add metrics for assessing uncaught IntrumentationTest crash failures (test level).
+        InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.TEST_CRASH_FAILURES, 1);
+        if (FailureStatus.UNSET.equals(failure.getFailureStatus())) {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.UNCAUGHT_TEST_CRASH_FAILURES, 1);
+        }
         super.testFailed(test, failure);
     }
 
@@ -105,7 +122,13 @@
         }
         error.setErrorMessage(errorMessage);
         if (isCrash(errorMessage)) {
-            error.setErrorIdentifier(DeviceErrorIdentifier.INSTRUMENATION_CRASH);
+            error.setErrorIdentifier(DeviceErrorIdentifier.INSTRUMENTATION_CRASH);
+        }
+        // Add metrics for assessing uncaught IntrumentationTest crash failures.
+        InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.CRASH_FAILURES, 1);
+        if (FailureStatus.UNSET.equals(error.getFailureStatus())) {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.UNCAUGHT_CRASH_FAILURES, 1);
         }
         super.testRunFailed(error);
     }
@@ -129,6 +152,14 @@
         return errorMessage.contains(ERROR_MESSAGE) || errorMessage.contains(SYSTEM_CRASH_MESSAGE);
     }
 
+    private boolean isTimeout(String errorMessage) {
+        for (String timeoutMessage : TIMEOUT_MESSAGES) {
+            if (errorMessage.contains(timeoutMessage)) {
+                return true;
+            }
+        }
+        return false;
+    }
     /**
      * Extract a formatted object from the logcat snippet.
      *
diff --git a/src/com/android/tradefed/result/ResultForwarder.java b/src/com/android/tradefed/result/ResultForwarder.java
index ba38e54..c651405 100644
--- a/src/com/android/tradefed/result/ResultForwarder.java
+++ b/src/com/android/tradefed/result/ResultForwarder.java
@@ -341,6 +341,20 @@
     }
 
     @Override
+    public void testAssumptionFailure(TestDescription test, FailureDescription failure) {
+        for (ITestInvocationListener listener : mListeners) {
+            try {
+                listener.testAssumptionFailure(test, failure);
+            } catch (RuntimeException e) {
+                CLog.e(
+                        "Exception while invoking %s#testAssumptionFailure",
+                        listener.getClass().getName());
+                CLog.e(e);
+            }
+        }
+    }
+
+    @Override
     public void testIgnored(TestDescription test) {
         for (ITestInvocationListener listener : mListeners) {
             try {
diff --git a/src/com/android/tradefed/result/proto/FileProtoResultReporter.java b/src/com/android/tradefed/result/proto/FileProtoResultReporter.java
index 2011c90..3ccbead 100644
--- a/src/com/android/tradefed/result/proto/FileProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/FileProtoResultReporter.java
@@ -92,6 +92,11 @@
         mPeriodicWriting = enabled;
     }
 
+    /** Whether or not periodic writing is enabled. */
+    public boolean isPeriodicWriting() {
+        return mPeriodicWriting;
+    }
+
     private void writeProto(TestRecord record) {
         if (mOutputFile == null) {
             return;
diff --git a/src/com/android/tradefed/result/suite/SuiteResultReporter.java b/src/com/android/tradefed/result/suite/SuiteResultReporter.java
index 424cd9d..82e4685 100644
--- a/src/com/android/tradefed/result/suite/SuiteResultReporter.java
+++ b/src/com/android/tradefed/result/suite/SuiteResultReporter.java
@@ -401,6 +401,9 @@
 
     @Override
     public TestSummary getSummary() {
+        if (mSummary == null || mSummary.toString().isEmpty()) {
+            return null;
+        }
         TestSummary summary = new TestSummary(new TypedString(mSummary.toString(), Type.TEXT));
         summary.setSource(SUITE_REPORTER_SOURCE);
         return summary;
diff --git a/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java b/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
index 772075b..428199d 100644
--- a/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
+++ b/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
@@ -304,7 +304,7 @@
                     message = "Run was incomplete. Some tests might not have finished.";
                 }
                 serializer.startTag(NS, MODULES_NOT_DONE_REASON);
-                serializer.attribute(NS, MESSAGE_ATTR, message);
+                serializer.attribute(NS, MESSAGE_ATTR, sanitizeXmlContent(message));
                 serializer.endTag(NS, MODULES_NOT_DONE_REASON);
             }
             serializeTestCases(serializer, module.getTestResults());
diff --git a/src/com/android/tradefed/retry/BaseRetryDecision.java b/src/com/android/tradefed/retry/BaseRetryDecision.java
index 0228ae9..599aed8 100644
--- a/src/com/android/tradefed/retry/BaseRetryDecision.java
+++ b/src/com/android/tradefed/retry/BaseRetryDecision.java
@@ -20,14 +20,20 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.StubDevice;
+import com.android.tradefed.device.cloud.RemoteAndroidVirtualDevice;
 import com.android.tradefed.invoker.IInvocationContext;
+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.TestDescription;
 import com.android.tradefed.result.TestResult;
 import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
+import com.android.tradefed.targetprep.TargetSetupError;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.ITestFilterReceiver;
 import com.android.tradefed.testtype.retry.IAutoRetriableTest;
+import com.android.tradefed.testtype.suite.ModuleDefinition;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -53,6 +59,13 @@
     private boolean mRebootAtLastRetry = false;
 
     @Option(
+            name = "reset-at-last-retry",
+            description =
+                    "Reset or powerwash the device at the last retry attempt. If this option is "
+                            + "set, option `reboot-at-last-retry` will be ignored.")
+    private boolean mResetAtLastRetry = false;
+
+    @Option(
         name = "max-testcase-run-count",
         description =
                 "If the IRemoteTest can have its testcases run multiple times, "
@@ -113,6 +126,16 @@
     public boolean shouldRetry(
             IRemoteTest test, int attemptJustExecuted, List<TestRunResult> previousResults)
             throws DeviceNotAvailableException {
+        return shouldRetry(test, null, attemptJustExecuted, previousResults);
+    }
+
+    @Override
+    public boolean shouldRetry(
+            IRemoteTest test,
+            ModuleDefinition module,
+            int attemptJustExecuted,
+            List<TestRunResult> previousResults)
+            throws DeviceNotAvailableException {
         // Keep track of some results for the test in progress for statistics purpose.
         if (test != mCurrentlyConsideredTest) {
             mCurrentlyConsideredTest = test;
@@ -143,7 +166,7 @@
             boolean shouldRetry = handleRetryFailures(filterableTest, previousResults);
             if (shouldRetry) {
                 // In case of retry, go through the recovery routine
-                recoverStateOfDevices(getDevices(), attemptJustExecuted);
+                recoverStateOfDevices(getDevices(), attemptJustExecuted, module);
             }
             return shouldRetry;
         } else if (test instanceof IAutoRetriableTest) {
@@ -295,13 +318,74 @@
     }
 
     /** Recovery attempt on the device to get it a better state before next retry. */
-    private void recoverStateOfDevices(List<ITestDevice> devices, int lastAttempt)
+    private void recoverStateOfDevices(
+            List<ITestDevice> devices, int lastAttempt, ModuleDefinition module)
             throws DeviceNotAvailableException {
+        if (lastAttempt == (mMaxRetryAttempts - 2)) {
+            if (mResetAtLastRetry) {
+                resetDevice(module, devices);
+            } else if (mRebootAtLastRetry) {
+                for (ITestDevice device : devices) {
+                    device.reboot();
+                    continue;
+                }
+            }
+        }
+    }
+
+    private void resetDevice(ModuleDefinition module, List<ITestDevice> devices)
+            throws DeviceNotAvailableException {
+        CLog.d("Reset devices...");
+        int deviceResetCount = 0;
         for (ITestDevice device : devices) {
-            if (mRebootAtLastRetry && (lastAttempt == (mMaxRetryAttempts - 2))) {
-                device.reboot();
+            if (!(device instanceof RemoteAndroidVirtualDevice)) {
+                CLog.i(
+                        "Device %s of type %s does not support powerwash.",
+                        device.getSerialNumber(), device.getClass());
                 continue;
             }
+            boolean success = false;
+            try {
+                success = ((RemoteAndroidVirtualDevice) device).powerwashGce();
+                deviceResetCount++;
+            } catch (TargetSetupError e) {
+                CLog.e(e);
+                throw new DeviceNotAvailableException(
+                        String.format(
+                                "Failed to powerwash device: %s\nError: %s",
+                                device.getSerialNumber(), e.toString()),
+                        e,
+                        device.getSerialNumber(),
+                        DeviceErrorIdentifier.DEVICE_FAILED_TO_RESET);
+            }
+
+            if (!success) {
+                throw new DeviceNotAvailableException(
+                        String.format("Failed to powerwash device: %s", device.getSerialNumber()),
+                        device.getSerialNumber(),
+                        DeviceErrorIdentifier.DEVICE_FAILED_TO_RESET);
+            }
+        }
+
+        if (module != null) {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.DEVICE_RESET_MODULES, module.getId());
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.DEVICE_RESET_COUNT, deviceResetCount);
+
+            // Run all preparers including suite level ones.
+            Throwable preparationException =
+                    module.runPreparation(true /* includeSuitePreparers */);
+            if (preparationException != null) {
+                CLog.e(preparationException);
+                throw new DeviceNotAvailableException(
+                        String.format(
+                                "Failed to reset devices before retry: %s",
+                                preparationException.toString()),
+                        preparationException,
+                        devices.get(0).getSerialNumber(),
+                        DeviceErrorIdentifier.DEVICE_FAILED_TO_RESET);
+            }
         }
     }
 }
diff --git a/src/com/android/tradefed/retry/IRetryDecision.java b/src/com/android/tradefed/retry/IRetryDecision.java
index 70b3e00..bc0face 100644
--- a/src/com/android/tradefed/retry/IRetryDecision.java
+++ b/src/com/android/tradefed/retry/IRetryDecision.java
@@ -19,6 +19,7 @@
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.suite.ModuleDefinition;
 
 import java.util.List;
 
@@ -58,6 +59,24 @@
             throws DeviceNotAvailableException;
 
     /**
+     * Decide whether or not retry should be attempted. Also make any necessary changes to the
+     * {@link IRemoteTest} to be retried (Applying filters, etc.).
+     *
+     * @param test The {@link IRemoteTest} that just ran.
+     * @param module The {@link ModuleDefinition} object for the test module.
+     * @param attemptJustExecuted The number of the attempt that we just ran.
+     * @param previousResults The list of {@link TestRunResult} of the test that just ran.
+     * @return True if we should retry, False otherwise.
+     * @throws DeviceNotAvailableException Can be thrown during device recovery
+     */
+    public boolean shouldRetry(
+            IRemoteTest test,
+            ModuleDefinition module,
+            int attemptJustExecuted,
+            List<TestRunResult> previousResults)
+            throws DeviceNotAvailableException;
+
+    /**
      * {@link #shouldRetry(IRemoteTest, int, List)} will most likely be called before the last retry
      * attempt, so we might be missing the very last attempt results for statistics purpose. This
      * method allows those results to be provided for proper statistics calculations.
diff --git a/src/com/android/tradefed/retry/ResultAggregator.java b/src/com/android/tradefed/retry/ResultAggregator.java
index 43848fd..b028bdf 100644
--- a/src/com/android/tradefed/retry/ResultAggregator.java
+++ b/src/com/android/tradefed/retry/ResultAggregator.java
@@ -257,6 +257,12 @@
     }
 
     @Override
+    public void testAssumptionFailure(TestDescription test, FailureDescription failure) {
+        super.testAssumptionFailure(test, failure);
+        mDetailedForwarder.testAssumptionFailure(test, failure);
+    }
+
+    @Override
     public void testFailed(TestDescription test, String trace) {
         super.testFailed(test, trace);
         mDetailedForwarder.testFailed(test, trace);
diff --git a/src/com/android/tradefed/sandbox/ISandbox.java b/src/com/android/tradefed/sandbox/ISandbox.java
index c78c1a9..ba0fc67 100644
--- a/src/com/android/tradefed/sandbox/ISandbox.java
+++ b/src/com/android/tradefed/sandbox/ISandbox.java
@@ -65,7 +65,7 @@
      */
     public File getTradefedSandboxEnvironment(
             IInvocationContext context, IConfiguration nonVersionedConfig, String[] args)
-            throws ConfigurationException;
+            throws Exception;
 
     /**
      * Create a classpath based on the environment and the working directory returned by {@link
diff --git a/src/com/android/tradefed/sandbox/TradefedSandbox.java b/src/com/android/tradefed/sandbox/TradefedSandbox.java
index 71b13a4..942955d 100644
--- a/src/com/android/tradefed/sandbox/TradefedSandbox.java
+++ b/src/com/android/tradefed/sandbox/TradefedSandbox.java
@@ -125,6 +125,7 @@
         // Allow interruption, subprocess should handle signals itself
         mRunUtil.allowInterrupt(true);
         CommandResult result = null;
+        RuntimeException interruptedException = null;
         try {
             result =
                     mRunUtil.runTimedCmd(
@@ -132,6 +133,7 @@
         } catch (RuntimeException interrupted) {
             CLog.e("Sandbox runtimedCmd threw an exception");
             CLog.e(interrupted);
+            interruptedException = interrupted;
             result = new CommandResult(CommandStatus.EXCEPTION);
             result.setStdout(StreamUtil.getStackTrace(interrupted));
         }
@@ -156,7 +158,9 @@
             } else {
                 joinResult = mEventParser.joinReceiver(waitTime);
             }
-
+            if (interruptedException != null) {
+                throw interruptedException;
+            }
             if (!joinResult) {
                 if (!failedStatus) {
                     result.setStatus(CommandStatus.EXCEPTION);
@@ -164,15 +168,15 @@
                 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 "
                                     + "under the name pattern '%s*'",
                             SANDBOX_PREFIX));
         } finally {
+            if (mProtoReceiver != null) {
+                mProtoReceiver.completeModuleEvents();
+            }
             // Log the configuration used to run
             try (InputStreamSource configFile =
                     new FileInputStreamSource(mSerializedConfiguration)) {
@@ -252,7 +256,7 @@
                                     config.getCommandLine(),
                                     /** no logging */
                                     false));
-        } catch (ConfigurationException e) {
+        } catch (Exception e) {
             return e;
         }
 
@@ -289,7 +293,7 @@
     @Override
     public File getTradefedSandboxEnvironment(
             IInvocationContext context, IConfiguration nonVersionedConfig, String[] args)
-            throws ConfigurationException {
+            throws Exception {
         SandboxOptions options = getSandboxOptions(nonVersionedConfig);
         // Check that we have no args conflicts.
         if (options.getSandboxTfDirectory() != null && options.getSandboxBuildId() != null) {
diff --git a/src/com/android/tradefed/targetprep/DeviceFailedToBootError.java b/src/com/android/tradefed/targetprep/DeviceFailedToBootError.java
index 45cfbc6..5e741a2 100644
--- a/src/com/android/tradefed/targetprep/DeviceFailedToBootError.java
+++ b/src/com/android/tradefed/targetprep/DeviceFailedToBootError.java
@@ -21,15 +21,19 @@
 /**
  * Thrown if a device fails to boot after being flashed with a build.
  */
-@SuppressWarnings("serial")
 public class DeviceFailedToBootError extends BuildError {
 
+    private static final long serialVersionUID = -6539557027017640715L;
+
     /**
      * Constructs a new (@link DeviceFailedToBootError} with a detailed error message.
      *
      * @param reason an error message giving more details about the boot failure
      * @param descriptor the descriptor of the device concerned by the exception
+     * @deprecated Use {@link #DeviceFailedToBootError(String, DeviceDescriptor, ErrorIdentifier)}
+     *     instead
      */
+    @Deprecated
     public DeviceFailedToBootError(String reason, DeviceDescriptor descriptor) {
         super(reason, descriptor);
     }
diff --git a/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java b/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
index 350c1e9..00f7913 100644
--- a/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
+++ b/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
@@ -264,10 +264,12 @@
                 device.waitForDeviceAvailable(mDeviceBootTime);
             } catch (DeviceUnresponsiveException e) {
                 // assume this is a build problem
-                throw new DeviceFailedToBootError(String.format(
-                        "Device %s did not become available after flashing %s",
-                        device.getSerialNumber(), deviceBuild.getDeviceBuildId()),
-                        device.getDeviceDescriptor());
+                throw new DeviceFailedToBootError(
+                        String.format(
+                                "Device %s did not become available after flashing %s",
+                                device.getSerialNumber(), deviceBuild.getDeviceBuildId()),
+                        device.getDeviceDescriptor(),
+                        DeviceErrorIdentifier.ERROR_AFTER_FLASHING);
             }
             device.postBootSetup();
         } finally {
diff --git a/src/com/android/tradefed/targetprep/DeviceSetup.java b/src/com/android/tradefed/targetprep/DeviceSetup.java
index 2db82ba..539ecff 100644
--- a/src/com/android/tradefed/targetprep/DeviceSetup.java
+++ b/src/com/android/tradefed/targetprep/DeviceSetup.java
@@ -746,6 +746,21 @@
         device.executeShellCommand("chmod 644 /data/local.prop");
         CLog.i("Rebooting %s due to system property change", device.getSerialNumber());
         device.reboot();
+
+        // Verify properties have expected values.
+        List<String> unmatched = new ArrayList<>();
+        for (Map.Entry<String, String> prop : mSetProps.entrySet()) {
+            String actual = device.getProperty(prop.getKey());
+            String expected = prop.getValue();
+            if (!actual.equals(expected)) {
+                unmatched.add(String.format("%s=%s(expected:%s)", prop.getKey(), actual, expected));
+            }
+        }
+        if (unmatched.size() > 0) {
+            throw new TargetSetupError(
+                    String.format("Failed to set properties: %s", unmatched),
+                    device.getDeviceDescriptor());
+        }
     }
 
     /**
@@ -894,7 +909,7 @@
             CLog.d("Skipping connect wifi due to force-skip-run-commands");
             return;
         }
-        if (mWifiSsid == null && mWifiSsidToPsk.isEmpty()) {
+        if ((mWifiSsid == null || mWifiSsid.isEmpty()) && mWifiSsidToPsk.isEmpty()) {
             return;
         }
 
diff --git a/src/com/android/tradefed/targetprep/DeviceUpdateTargetPreparer.java b/src/com/android/tradefed/targetprep/DeviceUpdateTargetPreparer.java
index e6ff4cc..3218365 100644
--- a/src/com/android/tradefed/targetprep/DeviceUpdateTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/DeviceUpdateTargetPreparer.java
@@ -25,6 +25,7 @@
 import com.android.tradefed.device.ITestDevice.RecoveryMode;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.result.error.InfraErrorIdentifier;
 
 import java.io.File;
@@ -66,7 +67,7 @@
             throw new TargetSetupError(
                     "Device image file not found: " + deviceUpdateImage.getAbsolutePath(),
                     device.getDeviceDescriptor(),
-                    InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+                    InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
         }
         preUpdateActions(deviceUpdateImage, device);
         // flashing concurrency control
@@ -106,7 +107,8 @@
                     String.format(
                             "Device %s did not become available after flashing %s",
                             device.getSerialNumber(), deviceUpdateImage.getAbsolutePath()),
-                    device.getDeviceDescriptor());
+                    device.getDeviceDescriptor(),
+                    DeviceErrorIdentifier.ERROR_AFTER_FLASHING);
         }
         CLog.i("Device update completed on %s", device.getDeviceDescriptor());
         // calling this last because we want to inject device side build info after device boots up
diff --git a/src/com/android/tradefed/targetprep/FlashingResourcesParser.java b/src/com/android/tradefed/targetprep/FlashingResourcesParser.java
index 87d6ee6..51af0d9 100644
--- a/src/com/android/tradefed/targetprep/FlashingResourcesParser.java
+++ b/src/com/android/tradefed/targetprep/FlashingResourcesParser.java
@@ -277,7 +277,7 @@
                                 "Could not find %s in device image zip %s",
                                 ANDROID_INFO_FILE_NAME, deviceImgZipFile.getName()),
                         nullDescriptor,
-                        InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+                        InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
             }
             infoReader = new BufferedReader(new InputStreamReader(
                     deviceZip.getInputStream(androidInfoEntry)));
diff --git a/src/com/android/tradefed/targetprep/GkiDeviceFlashPreparer.java b/src/com/android/tradefed/targetprep/GkiDeviceFlashPreparer.java
index 72a89d9..d896f00 100644
--- a/src/com/android/tradefed/targetprep/GkiDeviceFlashPreparer.java
+++ b/src/com/android/tradefed/targetprep/GkiDeviceFlashPreparer.java
@@ -15,7 +15,7 @@
  */
 package com.android.tradefed.targetprep;
 
-import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
@@ -26,6 +26,7 @@
 import com.android.tradefed.device.ITestDevice.RecoveryMode;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
@@ -79,7 +80,7 @@
     public void setUp(TestInformation testInfo)
             throws TargetSetupError, BuildError, DeviceNotAvailableException {
         ITestDevice device = testInfo.getDevice();
-        IDeviceBuildInfo buildInfo = (IDeviceBuildInfo) testInfo.getBuildInfo();
+        IBuildInfo buildInfo = testInfo.getBuildInfo();
 
         File tmpDir = null;
         try {
@@ -106,7 +107,8 @@
                     String.format(
                             "Device %s did not become available after flashing GKI. Exception: %s",
                             device.getSerialNumber(), e),
-                    device.getDeviceDescriptor());
+                    device.getDeviceDescriptor(),
+                    DeviceErrorIdentifier.ERROR_AFTER_FLASHING);
         }
         device.postBootSetup();
         CLog.i("Device update completed on %s", device.getDeviceDescriptor());
@@ -136,10 +138,10 @@
      * Flash GKI images.
      *
      * @param device the {@link ITestDevice}
-     * @param buildInfo the {@link IDeviceBuildInfo} the device build info
+     * @param buildInfo the {@link IBuildInfo} the build info
      * @throws TargetSetupError, DeviceNotAvailableException, IOException
      */
-    private void flashGki(ITestDevice device, IDeviceBuildInfo buildInfo)
+    private void flashGki(ITestDevice device, IBuildInfo buildInfo)
             throws TargetSetupError, DeviceNotAvailableException {
         IDeviceManager deviceManager = getDeviceManager();
         device.waitForDeviceOnline();
@@ -178,13 +180,13 @@
      * Validate GKI boot image is expected. (Obsoleted. Please call with tmpDir provided)
      *
      * @param device the {@link ITestDevice}
-     * @param buildInfo the {@link IDeviceBuildInfo} the device build info
+     * @param buildInfo the {@link IBuildInfo} the build info
      * @throws TargetSetupError if there is no valid gki boot.img
      */
-    public void validateGkiBootImg(ITestDevice device, IDeviceBuildInfo buildInfo)
+    public void validateGkiBootImg(ITestDevice device, IBuildInfo buildInfo)
             throws TargetSetupError {
         throw new TargetSetupError(
-                "Obsoleted. Please use validateGkiBootImg(ITestDevice, IDeviceBuildInfo, File)",
+                "Obsoleted. Please use validateGkiBootImg(ITestDevice, IBuildInfo, File)",
                 device.getDeviceDescriptor());
     }
 
@@ -192,12 +194,12 @@
      * Validate GKI boot image is expected. Throw exception if there is no valid boot.img.
      *
      * @param device the {@link ITestDevice}
-     * @param buildInfo the {@link IDeviceBuildInfo} the device build info
+     * @param buildInfo the {@link IBuildInfo} the build info
      * @param tmpDir the temporary directory {@link File}
      * @throws TargetSetupError if there is no valid gki boot.img
      */
     @VisibleForTesting
-    protected void validateGkiBootImg(ITestDevice device, IDeviceBuildInfo buildInfo, File tmpDir)
+    protected void validateGkiBootImg(ITestDevice device, IBuildInfo buildInfo, File tmpDir)
             throws TargetSetupError {
         if (buildInfo.getFile(GKI_BOOT_IMG) != null && mBootImageFileName != null) {
             mBootImg =
@@ -246,40 +248,6 @@
     }
 
     /**
-     * Flash device images.
-     *
-     * @param device the {@link ITestDevice}
-     * @param buildInfo the {@link IDeviceBuildInfo} the device build info
-     * @throws TargetSetupError, DeviceNotAvailableException
-     */
-    private void flashDeviceImage(ITestDevice device, IDeviceBuildInfo buildInfo)
-            throws TargetSetupError, DeviceNotAvailableException {
-        IDeviceManager deviceManager = getDeviceManager();
-        long start = System.currentTimeMillis();
-        deviceManager.takeFlashingPermit();
-        CLog.v(
-                "Flashing permit obtained after %ds",
-                TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis() - start)));
-        // don't allow interruptions during flashing operations.
-        getRunUtil().allowInterrupt(false);
-        try {
-            executeFastbootCmd(
-                    device,
-                    "--skip-reboot",
-                    "--disable-verity",
-                    "update",
-                    buildInfo.getDeviceImageFile().getAbsolutePath());
-        } finally {
-            // Allow interruption at the end no matter what.
-            getRunUtil().allowInterrupt(true);
-            deviceManager.returnFlashingPermit();
-            CLog.v(
-                    "Flashing permit returned after %ds",
-                    TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis() - start)));
-        }
-    }
-
-    /**
      * Helper method to execute host command.
      *
      * @param device the {@link ITestDevice}
@@ -338,7 +306,10 @@
                 ZipUtil2.extractZip(sourceFile, destDir);
                 requestedFile = FileUtil.findFile(destDir, requestedFileName);
             } catch (IOException e) {
-                throw new TargetSetupError(e.getMessage(), e, device.getDeviceDescriptor());
+                throw new TargetSetupError(
+                        String.format("Fail to get %s from %s", requestedFileName, sourceFile),
+                        e,
+                        device.getDeviceDescriptor());
             }
         } else if (sourceFile.isDirectory()) {
             requestedFile = FileUtil.findFile(sourceFile, requestedFileName);
@@ -381,7 +352,7 @@
             throw new TargetSetupError(
                     String.format(
                             "fastboot command %s failed in device %s. stdout: %s, stderr: %s",
-                            cmdArgs[0],
+                            cmdArgs,
                             device.getSerialNumber(),
                             result.getStdout(),
                             result.getStderr()),
diff --git a/src/com/android/tradefed/targetprep/GsiDeviceFlashPreparer.java b/src/com/android/tradefed/targetprep/GsiDeviceFlashPreparer.java
index 23ce54d..25bb99f 100644
--- a/src/com/android/tradefed/targetprep/GsiDeviceFlashPreparer.java
+++ b/src/com/android/tradefed/targetprep/GsiDeviceFlashPreparer.java
@@ -15,7 +15,7 @@
  */
 package com.android.tradefed.targetprep;
 
-import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
@@ -26,6 +26,7 @@
 import com.android.tradefed.device.ITestDevice.RecoveryMode;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
@@ -48,9 +49,6 @@
 @OptionClass(alias = "gsi-device-flash-preparer")
 public class GsiDeviceFlashPreparer extends BaseTargetPreparer {
 
-    private static final String GSI_SYSTEM_IMG = "gsi_system.img";
-    private static final String GSI_VBMETA_IMG = "gsi_vbmeta.img";
-    private static final String GKI_BOOT_IMG = "gki_boot.img";
     private static final int DYNAMIC_PARTITION_API_LEVEL = 29;
     // Wait time for device state to stablize in millisecond
     private static final int STATE_STABLIZATION_WAIT_TIME_MLLISECS = 60000;
@@ -62,26 +60,40 @@
     private long mDeviceBootTime = 5 * 60 * 1000;
 
     @Option(
+            name = "system-image-zip-name",
+            description = "The name of the zip file containing the system image in BuildInfo.")
+    private String mSystemImageZipName = "gsi_system.img";
+
+    @Option(
             name = "system-image-file-name",
             description =
-                    "The system image file name to search for if provided gsi_system.img is in "
-                            + "a zip file or directory.")
+                    "The system image file name to search for if provided system image "
+                            + "is in a zip file or directory.")
     private String mSystemImageFileName = "system.img";
 
     @Option(
+            name = "vbmeta-image-zip-name",
+            description = "The name of the zip file containing the system image in BuildInfo.")
+    private String mVbmetaImageZipName = "gsi_vbmeta.img";
+
+    @Option(
             name = "vbmeta-image-file-name",
             description =
-                    "The vbmeta image file name to search for if provided gsi_vbmeta.img is in "
-                            + "a zip file or directory.")
+                    "The vbmeta image file name to search for if provided vbmeta image is "
+                            + "in a zip file or directory.")
     private String mVbmetaImageFileName = "vbmeta.img";
 
     @Option(
+            name = "boot-image-zip-name",
+            description = "The name of the zip file containing the boot image in BuildInfo.")
+    private String mBootImageZipName = "gki_boot.img";
+
+    @Option(
             name = "boot-image-file-name",
             description =
-                    "The boot image file name to search for if gki_boot.img is provided in BuildInfo and the provided "
-                            + "file is a zip file or directory, for example boot-5.4.img. By default when gki_boot.img "
-                            + "is provided in BuildInfo with a zip file or file directory, the target preparer will use"
-                            + " the first found file that matches boot(.*).img as file name.")
+                    "The boot image file name to search for if boot image is is in a zip "
+                            + "file or directory, for example boot-5.4.img. The first file"
+                            + "match the provided name string will be used.")
     private String mBootImageFileName = "boot(.*).img";
 
     @Option(
@@ -98,11 +110,11 @@
     public void setUp(TestInformation testInfo)
             throws TargetSetupError, BuildError, DeviceNotAvailableException {
         ITestDevice device = testInfo.getDevice();
-        IDeviceBuildInfo buildInfo = (IDeviceBuildInfo) testInfo.getBuildInfo();
+        IBuildInfo buildInfo = testInfo.getBuildInfo();
 
         File tmpDir = null;
         try {
-            tmpDir = FileUtil.createTempDir("gki_preparer");
+            tmpDir = FileUtil.createTempDir("gsi_preparer");
             validateGsiImg(device, buildInfo, tmpDir);
             flashGsi(device, buildInfo);
         } catch (IOException ioe) {
@@ -125,7 +137,8 @@
                     String.format(
                             "Device %s did not become available after flashing GKI. Exception: %s",
                             device.getSerialNumber(), e),
-                    device.getDeviceDescriptor());
+                    device.getDeviceDescriptor(),
+                    DeviceErrorIdentifier.ERROR_AFTER_FLASHING);
         }
         device.postBootSetup();
         CLog.i("Device update completed on %s", device.getDeviceDescriptor());
@@ -155,10 +168,10 @@
      * Flash GSI images.
      *
      * @param device the {@link ITestDevice}
-     * @param buildInfo the {@link IDeviceBuildInfo} the device build info
+     * @param buildInfo the {@link IBuildInfo} the build info
      * @throws TargetSetupError, DeviceNotAvailableException, IOException
      */
-    private void flashGsi(ITestDevice device, IDeviceBuildInfo buildInfo)
+    private void flashGsi(ITestDevice device, IBuildInfo buildInfo)
             throws TargetSetupError, DeviceNotAvailableException {
         IDeviceManager deviceManager = getDeviceManager();
         device.waitForDeviceOnline();
@@ -196,11 +209,10 @@
                 if (shouldUseFastbootd) {
                     device.rebootIntoFastbootd();
                     if (mShouldEraseProductPartition) {
-                        executeFastbootCmd(
-                                device, "delete-logical-partition", "product" + currSlot);
+                        device.executeLongFastbootCommand(
+                                "delete-logical-partition", "product" + currSlot);
                     }
                 }
-                String systemPartition = "system";
                 executeFastbootCmd(device, "erase", "system" + currSlot);
                 executeFastbootCmd(device, "flash", "system", mSystemImg.getAbsolutePath());
                 executeFastbootCmd(device, "-w");
@@ -223,33 +235,38 @@
      * Validate GSI image is expected. Throw exception if there is no valid GSI image.
      *
      * @param device the {@link ITestDevice}
-     * @param buildInfo the {@link IDeviceBuildInfo} the device build info
+     * @param buildInfo the {@link IBuildInfo} the build info
      * @param tmpDir the temporary directory {@link File}
      * @throws TargetSetupError if there is no valid gki boot.img
      */
-    private void validateGsiImg(ITestDevice device, IDeviceBuildInfo buildInfo, File tmpDir)
+    private void validateGsiImg(ITestDevice device, IBuildInfo buildInfo, File tmpDir)
             throws TargetSetupError {
-        if (buildInfo.getFile(GSI_SYSTEM_IMG) == null) {
+        if (buildInfo.getFile(mSystemImageZipName) == null) {
             throw new TargetSetupError(
-                    String.format("BuildInfo doesn't contain file key %s.", GSI_SYSTEM_IMG),
-                    device.getDeviceDescriptor());
-        }
-        if (buildInfo.getFile(GSI_VBMETA_IMG) == null) {
-            throw new TargetSetupError(
-                    String.format("BuildInfo doesn't contain file key %s.", GSI_VBMETA_IMG),
+                    String.format("BuildInfo doesn't contain file key %s.", mSystemImageZipName),
                     device.getDeviceDescriptor());
         }
         mSystemImg =
                 getRequestedFile(
-                        device, mSystemImageFileName, buildInfo.getFile(GSI_SYSTEM_IMG), tmpDir);
-        mVbmetaImg =
-                getRequestedFile(
-                        device, mVbmetaImageFileName, buildInfo.getFile(GSI_VBMETA_IMG), tmpDir);
-
-        if (buildInfo.getFile(GKI_BOOT_IMG) != null && mBootImageFileName != null) {
+                        device,
+                        mSystemImageFileName,
+                        buildInfo.getFile(mSystemImageZipName),
+                        tmpDir);
+        if (buildInfo.getFile(mVbmetaImageZipName) != null) {
+            mVbmetaImg =
+                    getRequestedFile(
+                            device,
+                            mVbmetaImageFileName,
+                            buildInfo.getFile(mVbmetaImageZipName),
+                            tmpDir);
+        }
+        if (buildInfo.getFile(mBootImageZipName) != null && mBootImageFileName != null) {
             mBootImg =
                     getRequestedFile(
-                            device, mBootImageFileName, buildInfo.getFile(GKI_BOOT_IMG), tmpDir);
+                            device,
+                            mBootImageFileName,
+                            buildInfo.getFile(mBootImageZipName),
+                            tmpDir);
         }
     }
 
@@ -299,7 +316,10 @@
                 ZipUtil2.extractZip(sourceFile, destDir);
                 requestedFile = FileUtil.findFile(destDir, requestedFileName);
             } catch (IOException e) {
-                throw new TargetSetupError(e.getMessage(), e, device.getDeviceDescriptor());
+                throw new TargetSetupError(
+                        String.format("Fail to get %s from %s", requestedFileName, sourceFile),
+                        e,
+                        device.getDeviceDescriptor());
             }
         } else if (sourceFile.isDirectory()) {
             requestedFile = FileUtil.findFile(sourceFile, requestedFileName);
@@ -342,7 +362,7 @@
             throw new TargetSetupError(
                     String.format(
                             "fastboot command %s failed in device %s. stdout: %s, stderr: %s",
-                            cmdArgs[0],
+                            cmdArgs,
                             device.getSerialNumber(),
                             result.getStdout(),
                             result.getStderr()),
diff --git a/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java b/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
index d6db235..936df0f 100644
--- a/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
@@ -65,15 +65,20 @@
     private static final int R_SDK_INT = 30;
 
     private List<ApexInfo> mTestApexInfoList = new ArrayList<>();
+    private List<ApexInfo> mModulesToUninstall = new ArrayList<>();
     private Set<String> mApkToInstall = new LinkedHashSet<>();
     private List<String> mApkInstalled = new ArrayList<>();
     private List<String> mSplitsInstallArgs = new ArrayList<>();
     private BundletoolUtil mBundletoolUtil;
     private String mDeviceSpecFilePath = "";
+    private boolean mOptimizeMainlineTest = false;
 
     @Option(name = "bundletool-file-name", description = "The file name of the bundletool jar.")
     private String mBundletoolFilename;
 
+    @Option(name = "train-path", description = "The absoulte path of the train folder.")
+    private File mTrainFolderPath;
+
     @Option(
         name = "apex-staging-wait-time",
         description = "The time in ms to wait for apex staged session ready.",
@@ -88,18 +93,36 @@
                             + "preloaded on device. Otherwise an exception will be thrown.")
     private boolean mIgnoreIfNotPreloaded = false;
 
+    @Option(
+            name = "skip-apex-teardown",
+            description = "Skip teardown if all files to be installed are apex files. "
+                    + "Currently, this option is only used for Test Mapping use case.")
+    private boolean mSkipApexTearDown = false;
+
     @Override
     public void setUp(TestInformation testInfo)
             throws TargetSetupError, BuildError, DeviceNotAvailableException {
         setTestInformation(testInfo);
         ITestDevice device = testInfo.getDevice();
 
-        if (getTestsFileName().isEmpty()) {
+        if (mTrainFolderPath != null) {
+            addApksToTestFiles();
+        }
+
+        List<File> moduleFileNames = getTestsFileName();
+        if (moduleFileNames.isEmpty()) {
             CLog.i("No apk/apex module file to install. Skipping.");
             return;
         }
 
-        cleanUpStagedAndActiveSession(device);
+        if (!mSkipApexTearDown || hasApkFilesToInstall(moduleFileNames)) {
+            // Cleanup the device if skip-apex-teardown isn't set or not all files to be installed
+            // are apex files. It will always run with the target preparer.
+            cleanUpStagedAndActiveSession(device);
+        }
+        else {
+            mOptimizeMainlineTest = true;
+        }
 
         Set<ApexInfo> activatedApexes = device.getActiveApexes();
 
@@ -113,9 +136,36 @@
             CLog.i("No modules are preloaded on the device, so no modules will be installed.");
             return;
         }
+
+        if (mOptimizeMainlineTest) {
+            CLog.i("Optimizing install apex module target preparer.");
+            // Get the apex files that are already installed on the device.
+            Set<ApexInfo> apexInData = getApexInData(activatedApexes);
+
+            // Get the apex files that are not used by the current test and will be uninstalled.
+            mModulesToUninstall.addAll(
+                    getModulesToUninstall(apexInData, testAppFiles, device));
+
+            for (ApexInfo m : mModulesToUninstall) {
+                CLog.i("Uninstalling module: %s", m.name);
+                super.uninstallPackage(device, m.name);
+            }
+
+            if (testAppFiles.isEmpty()) {
+                if (!mModulesToUninstall.isEmpty()) {
+                    RunUtil.getDefault().sleep(mApexStagingWaitTime);
+                    device.reboot();
+                }
+                // If both the list of files to be installed and uninstalled are empty, that means
+                // the mainline modules are the same as the previous ones.
+                CLog.i("All required modules are installed");
+                return;
+            }
+        }
+
         if (containsApks(testAppFiles)) {
             installUsingBundleTool(testInfo, testAppFiles);
-            if (mTestApexInfoList.isEmpty()) {
+            if (mTestApexInfoList.isEmpty() && mModulesToUninstall.isEmpty()) {
                 CLog.i("No Apex module in the train. Skipping reboot.");
                 return;
             } else {
@@ -158,13 +208,75 @@
                     String.format(
                             "Failed to activate %s on device %s.",
                             listApexInfo(failToActivateApex).toString(), device.getSerialNumber()),
-                    device.getDeviceDescriptor());
+                    device.getDeviceDescriptor(),
+                    DeviceErrorIdentifier.FAIL_ACTIVATE_APEX);
         }
         CLog.i("Train activation succeed.");
     }
 
+    /**
+     * Get a set of modules that will be uninstalled.
+     *
+     * @param apexInData A Set<ApexInfo> of modules that are installed on the /data directory.
+     * @param testFiles A List<File> of modules that will be installed on the device.
+     * @param device the {@link ITestDevice}
+     * @return A Set<ApexInfo> of modules that will be uninstalled on the device.
+     */
+    @VisibleForTesting
+    Set<ApexInfo> getModulesToUninstall(Set<ApexInfo> apexInData,
+            List<File> testFiles, ITestDevice device) throws TargetSetupError {
+        Set<ApexInfo> unInstallModules = new HashSet<>(apexInData);
+        List<File> filesToSkipInstall = new ArrayList<>();
+        for (File testFile : testFiles) {
+            String packageName = parsePackageName(testFile, device.getDeviceDescriptor());
+            for (ApexInfo apexModule : apexInData) {
+                if (apexModule.name.equals(packageName)) {
+                    unInstallModules.remove(apexModule);
+                    filesToSkipInstall.add(testFile);
+                }
+            }
+        }
+        // Update the modules to be installed based on what will not be installed.
+        testFiles.removeAll(filesToSkipInstall);
+        return unInstallModules;
+    }
+
+    /**
+     * Return a set of files that is already installed on the /data directory.
+     */
+    @VisibleForTesting
+    Set<ApexInfo> getApexInData(Set<ApexInfo> activatedApexes) {
+        Set<ApexInfo> apexInData = new HashSet<>();
+        for (ApexInfo apex : activatedApexes) {
+            if (apex.sourceDir.startsWith(ACTIVATED_APEX_SOURCEDIR_PREFIX, 1)) {
+                apexInData.add(apex);
+            }
+        }
+        return apexInData;
+    }
+
+    /**
+     * Check if the files to be installed contain .apk or .apks.
+     *
+     * @param testAppFiles List<File> of the modules that will be installed on the device.
+     * @return true if the files contain .apk or .apks, otherwise false.
+     */
+    private boolean hasApkFilesToInstall(List<File> testAppFiles) {
+        List<String> checkLists = Arrays.asList(".apk", ".apks");
+        for (File testAppFile : testAppFiles) {
+            if (checkLists.stream().anyMatch(entry -> testAppFile.getName().endsWith(entry))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     @Override
     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
+        if (mOptimizeMainlineTest) {
+            CLog.d("Skipping tearDown since the installed modules may be used for the next test.");
+            return;
+        }
         ITestDevice device = testInfo.getDevice();
         if (e instanceof DeviceNotAvailableException) {
             CLog.e("Device %s is not available. Teardown() skipped.", device.getSerialNumber());
@@ -193,12 +305,18 @@
         if (mBundletoolUtil != null) {
             return;
         }
-        File bundletoolJar = getLocalPathForFilename(testInfo, getBundletoolFileName());
+        File bundletoolJar;
+        File f = new File(getBundletoolFileName());
+        if (!f.isAbsolute()) {
+            bundletoolJar = getLocalPathForFilename(testInfo, getBundletoolFileName());
+        } else {
+            bundletoolJar = f;
+        }
         if (bundletoolJar == null) {
             throw new TargetSetupError(
                     String.format("Failed to find bundletool jar %s.", getBundletoolFileName()),
                     testInfo.getDevice().getDeviceDescriptor(),
-                    InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+                    InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
         }
         mBundletoolUtil = new BundletoolUtil(bundletoolJar);
     }
@@ -272,7 +390,10 @@
         List<File> moduleNamesToInstall = new ArrayList<>();
         for (File moduleFileName : moduleFileNames) {
             // getLocalPathForFilename throws if apk not found
-            File moduleFile = getLocalPathForFilename(testInfo, moduleFileName.getName());
+            File moduleFile = moduleFileName;
+            if (!moduleFile.isAbsolute()) {
+                moduleFile = getLocalPathForFilename(testInfo, moduleFileName.getName());
+            }
             String modulePackageName = "";
             if (moduleFile.getName().endsWith(SPLIT_APKS_SUFFIX)) {
                 List<File> splits = getSplitsForApks(testInfo, moduleFile);
@@ -470,7 +591,12 @@
             throws TargetSetupError, DeviceNotAvailableException {
         ITestDevice device = testInfo.getDevice();
         for (File moduleFileName : testAppFileNames) {
-            File moduleFile = getLocalPathForFilename(testInfo, moduleFileName.getName());
+            File moduleFile;
+            if (!moduleFileName.isAbsolute()) {
+                moduleFile = getLocalPathForFilename(testInfo, moduleFileName.getName());
+            } else {
+                moduleFile = moduleFileName;
+            }
             if (moduleFileName.getName().endsWith(SPLIT_APKS_SUFFIX)) {
                 List<File> splits = getSplitsForApks(testInfo, moduleFile);
                 String splitsArgs = createInstallArgsForSplit(splits, device);
@@ -727,6 +853,16 @@
         return failToActivateApex;
     }
 
+    private void addApksToTestFiles() {
+        File[] filesUnderTrainFolder = mTrainFolderPath.listFiles();
+        Arrays.sort(filesUnderTrainFolder, (a, b) -> a.getName().compareTo(b.getName()));
+        for (File f : filesUnderTrainFolder) {
+            if (f.getName().endsWith(".apks")) {
+                getTestsFileName().add(f);
+            }
+        }
+    }
+
     @VisibleForTesting
     protected String getBundletoolFileName() {
         return mBundletoolFilename;
@@ -741,4 +877,9 @@
     protected List<String> getApkInstalled() {
         return mApkInstalled;
     }
+
+    @VisibleForTesting
+    public void setSkipApexTearDown(boolean skip) {
+        mSkipApexTearDown = skip;
+    }
 }
diff --git a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
index fce3d8d..0490685 100644
--- a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
+++ b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
@@ -33,6 +33,7 @@
 import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.IAbiReceiver;
 import com.android.tradefed.util.AaptParser;
+import com.android.tradefed.util.AaptParser.AaptVersion;
 import com.android.tradefed.util.AbiFormatter;
 import com.android.tradefed.util.BuildTestsZipUtils;
 
@@ -152,6 +153,9 @@
     @Option(name = "instant-mode", description = "Whether or not to install apk in instant mode.")
     private boolean mInstantMode = false;
 
+    @Option(name = "aapt-version", description = "The version of AAPT for APK parsing.")
+    private AaptVersion mAaptVersion = AaptVersion.AAPT;
+
     @Option(
         name = "force-install-mode",
         description =
@@ -163,7 +167,7 @@
     private Integer mUserId = null;
     private Boolean mGrantPermission = null;
 
-    private Set<String> mPackagesInstalled = null;
+    private Set<String> mPackagesInstalled = new HashSet<>();
     private TestInformation mTestInfo;
 
     protected void setTestInformation(TestInformation testInfo) {
@@ -221,6 +225,11 @@
         mGrantPermission = shouldGrant;
     }
 
+    /** Sets the version of AAPT for APK parsing. */
+    public void setAaptVersion(AaptVersion aaptVersion) {
+        mAaptVersion = aaptVersion;
+    }
+
     /** Adds one apk installation arg to be used. */
     public void addInstallArg(String arg) {
         mInstallArgs.add(arg);
@@ -251,7 +260,7 @@
                             apkFileName, testInfo.getBuildInfo().toString()),
                     ioe,
                     testInfo.getDevice().getDeviceDescriptor(),
-                    InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+                    InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
         }
     }
 
@@ -277,9 +286,6 @@
             CLog.i("No test apps to install, skipping");
             return;
         }
-        if (mCleanup) {
-            mPackagesInstalled = new HashSet<>();
-        }
 
         // resolve abi flags
         if (mAbi != null && mForceAbi != null) {
@@ -370,7 +376,7 @@
     @Override
     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
         mTestInfo = testInfo;
-        if (mCleanup && mPackagesInstalled != null && !(e instanceof DeviceNotAvailableException)) {
+        if (mCleanup && !(e instanceof DeviceNotAvailableException)) {
             for (String packageName : mPackagesInstalled) {
                 try {
                     uninstallPackage(getDevice(), packageName);
@@ -471,7 +477,7 @@
                     throw new TargetSetupError(
                             String.format("Test app %s was not found.", apkFile.getName()),
                             device.getDeviceDescriptor(),
-                            InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+                            InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
                 } else {
                     CLog.d("Test app %s was not found.", apkFile.getName());
                     continue;
@@ -517,14 +523,16 @@
                     String.format(
                             "Could not list files of specified directory: %s", fileOrDirectory),
                     e,
-                    deviceDescriptor);
+                    deviceDescriptor,
+                    InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
         }
 
         if (mThrowIfNoFile && apkFiles.isEmpty()) {
             throw new TargetSetupError(
                     String.format(
                             "Could not find any files in specified directory: %s", fileOrDirectory),
-                    deviceDescriptor);
+                    deviceDescriptor,
+                    InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
         }
 
         return apkFiles;
@@ -588,7 +596,7 @@
     /** Get the package name from the test app. */
     protected String parsePackageName(File testAppFile, DeviceDescriptor deviceDescriptor)
             throws TargetSetupError {
-        AaptParser parser = AaptParser.parse(testAppFile);
+        AaptParser parser = AaptParser.parse(testAppFile, mAaptVersion);
         if (parser == null) {
             throw new TargetSetupError(
                     "apk installed but AaptParser failed",
@@ -598,4 +606,3 @@
         return parser.getPackageName();
     }
 }
-
diff --git a/src/com/android/tradefed/testtype/NoisyDryRunTest.java b/src/com/android/tradefed/testtype/NoisyDryRunTest.java
index 3e17cdf..9908ee7 100644
--- a/src/com/android/tradefed/testtype/NoisyDryRunTest.java
+++ b/src/com/android/tradefed/testtype/NoisyDryRunTest.java
@@ -20,11 +20,13 @@
 import com.android.tradefed.command.CommandFileParser;
 import com.android.tradefed.command.CommandFileParser.CommandLine;
 import com.android.tradefed.command.CommandOptions;
+import com.android.tradefed.command.CommandScheduler;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.ConfigurationFactory;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.SandboxConfigurationFactory;
+import com.android.tradefed.config.proxy.TradefedDelegator;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -147,6 +149,12 @@
             String[] args = commands.get(i).asArray();
             String cmdLine = QuotationAwareTokenizer.combineTokens(args);
             try {
+                TradefedDelegator delegator = CommandScheduler.checkDelegation(args);
+                if (delegator.shouldUseDelegation()) {
+                    // TODO: Add some validation of delegated config.
+                    continue;
+                }
+
                 if (cmdLine.contains("--" + CommandOptions.USE_SANDBOX)) {
                     // Handle the sandboxed command use case.
                     testSandboxCommand(args);
diff --git a/src/com/android/tradefed/testtype/SubprocessTfLauncher.java b/src/com/android/tradefed/testtype/SubprocessTfLauncher.java
index bbb8ea0..814ecf5 100644
--- a/src/com/android/tradefed/testtype/SubprocessTfLauncher.java
+++ b/src/com/android/tradefed/testtype/SubprocessTfLauncher.java
@@ -23,6 +23,7 @@
 import com.android.tradefed.config.IConfigurationReceiver;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.error.HarnessRuntimeException;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -31,6 +32,7 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.result.proto.StreamProtoReceiver;
 import com.android.tradefed.result.proto.StreamProtoResultReporter;
 import com.android.tradefed.util.CommandResult;
@@ -409,11 +411,12 @@
                 if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
                     errMessage = String.format("Timeout after %s",
                             TimeUtil.formatElapsedTime(mMaxTfRunTime));
-                    throw new RuntimeException(
+                    throw new HarnessRuntimeException(
                             String.format(
                                     "%s Tests subprocess failed due to:\n%s\n",
-                                    mConfigName, errMessage));
-                } else {
+                                    mConfigName, errMessage),
+                            InfraErrorIdentifier.INVOCATION_TIMEOUT);
+                } else if (eventParser != null && !eventParser.reportedInvocationFailed()) {
                     SubprocessExceptionParser.handleStderrException(result);
                 }
             }
diff --git a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
index 5c6348d..9ded188 100644
--- a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
@@ -284,6 +284,8 @@
             if (mEnableMainlineParameter) {
                 mModuleRepo.setMainlineParameterizedModules(mEnableMainlineParameter);
                 mModuleRepo.setInvocationContext(getInvocationContext());
+                mModuleRepo.setOptimizeMainlineTest(
+                        getConfiguration().getCommandOptions().getOptimizeMainlineTest());
             }
 
             mModuleRepo.setParameterizedModules(mEnableParameter);
diff --git a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
index 5f00853..5400ddd 100644
--- a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
+++ b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
@@ -17,6 +17,7 @@
 package com.android.tradefed.testtype.suite;
 
 import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.IConfigurationReceiver;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.DeviceUnresponsiveException;
 import com.android.tradefed.device.metric.CollectorHelper;
@@ -51,27 +52,28 @@
  * A wrapper class works on the {@link IRemoteTest} to granulate the IRemoteTest in testcase level.
  * An IRemoteTest can contain multiple testcases. Previously, these testcases are treated as a
  * whole: When IRemoteTest runs, all testcases will run. Some IRemoteTest (The ones that implements
- * ITestFilterReceiver) can accept a whitelist of testcases and only run those testcases. This class
- * takes advantage of the existing feature and provides a more flexible way to run test suite.
+ * ITestFilterReceiver) can accept an allowlist of testcases and only run those testcases. This
+ * class takes advantage of the existing feature and provides a more flexible way to run test suite.
  *
  * <ul>
- *   <li> Single testcase can be retried multiple times (within the same IRemoteTest run) to reduce
+ *   <li>Single testcase can be retried multiple times (within the same IRemoteTest run) to reduce
  *       the non-test-error failure rates.
- *   <li> The retried testcases are dynamically collected from previous run failures.
+ *   <li>The retried testcases are dynamically collected from previous run failures.
  * </ul>
  *
  * <p>Note:
  *
  * <ul>
- *   <li> The prerequisite to run a subset of test cases is that the test type should implement the
+ *   <li>The prerequisite to run a subset of test cases is that the test type should implement the
  *       interface {@link ITestFilterReceiver}.
- *   <li> X is customized max retry number.
+ *   <li>X is customized max retry number.
  * </ul>
  */
 public class GranularRetriableTestWrapper implements IRemoteTest, ITestCollector {
 
     private IRetryDecision mRetryDecision;
     private IRemoteTest mTest;
+    private ModuleDefinition mModule;
     private List<IMetricCollector> mRunMetricCollectors;
     private TestFailureListener mFailureListener;
     private IInvocationContext mModuleInvocationContext;
@@ -94,7 +96,18 @@
             TestFailureListener failureListener,
             List<ITestInvocationListener> moduleLevelListeners,
             int maxRunLimit) {
+        this(test, null, mainListener, failureListener, moduleLevelListeners, maxRunLimit);
+    }
+
+    public GranularRetriableTestWrapper(
+            IRemoteTest test,
+            ModuleDefinition module,
+            ITestInvocationListener mainListener,
+            TestFailureListener failureListener,
+            List<ITestInvocationListener> moduleLevelListeners,
+            int maxRunLimit) {
         mTest = test;
+        mModule = module;
         mMainGranularRunListener = new ModuleListener(mainListener);
         mFailureListener = failureListener;
         mModuleLevelListeners = moduleLevelListeners;
@@ -194,6 +207,9 @@
             if (collector.isDisabled()) {
                 CLog.d("%s has been disabled. Skipping.", collector);
             } else {
+                if (collector instanceof IConfigurationReceiver) {
+                    ((IConfigurationReceiver) collector).setConfiguration(mModuleConfiguration);
+                }
                 runListener = collector.init(mModuleInvocationContext, runListener);
             }
         }
@@ -226,7 +242,7 @@
 
         // Bail out early if there is no need to retry at all.
         if (!mRetryDecision.shouldRetry(
-                mTest, 0, mMainGranularRunListener.getTestRunForAttempts(0))) {
+                mTest, mModule, 0, mMainGranularRunListener.getTestRunForAttempts(0))) {
             return;
         }
         // Avoid rechecking the shouldRetry below the first time as it could retrigger reboot.
@@ -241,6 +257,7 @@
                     boolean retry =
                             mRetryDecision.shouldRetry(
                                     mTest,
+                                    mModule,
                                     attemptNumber - 1,
                                     mMainGranularRunListener.getTestRunForAttempts(
                                             attemptNumber - 1));
@@ -298,7 +315,10 @@
                             + "successful, proceeding with next module. Stack trace:");
             CLog.w(due);
             CLog.w("Proceeding to the next test.");
-            runListener.testRunFailed(createFromException(due));
+            // If it already was marked as failure do not remark it.
+            if (!mMainGranularRunListener.hasLastAttemptFailed()) {
+                runListener.testRunFailed(createFromException(due));
+            }
         } catch (DeviceNotAvailableException dnae) {
             // TODO: See if it's possible to report IReportNotExecuted
             CLog.e("Run in progress was not completed due to:");
diff --git a/src/com/android/tradefed/testtype/suite/ITestSuite.java b/src/com/android/tradefed/testtype/suite/ITestSuite.java
index 3561868..3183e9c 100644
--- a/src/com/android/tradefed/testtype/suite/ITestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/ITestSuite.java
@@ -69,13 +69,17 @@
 import com.android.tradefed.util.AbiFormatter;
 import com.android.tradefed.util.AbiUtils;
 import com.android.tradefed.util.MultiMap;
+import com.android.tradefed.util.StreamUtil;
 import com.android.tradefed.util.TimeUtil;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 
 import java.io.File;
 import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
 import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -131,6 +135,9 @@
 
     private static final String PRODUCT_CPU_ABI_KEY = "ro.product.cpu.abi";
 
+    private static final Set<String> ALLOWED_PREPARERS_CONFIGS =
+            ImmutableSet.of("/suite/allowed-preparers.txt", "/suite/google-allowed-preparers.txt");
+
     // Options for test failure case
     @Option(
         name = "bugreport-on-failure",
@@ -509,6 +516,9 @@
             return runModules;
         }
 
+        Map<String, List<ITargetPreparer>> suitePreparersPerDevice =
+                getAllowedPreparerPerDevice(mMainConfiguration);
+
         for (Entry<String, IConfiguration> config : runConfig.entrySet()) {
             // Validate the configuration, it will throw if not valid.
             ValidateSuiteConfigHelper.validateConfig(config.getValue());
@@ -519,6 +529,7 @@
                             config.getKey(),
                             config.getValue().getTests(),
                             preparersPerDevice,
+                            suitePreparersPerDevice,
                             config.getValue().getMultiTargetPreparers(),
                             config.getValue());
             if (mDisableAutoRetryTimeReporting) {
@@ -585,6 +596,40 @@
         return res;
     }
 
+    /** Create the mapping of device to its target_preparer that's allowed to rerun. */
+    private Map<String, List<ITargetPreparer>> getAllowedPreparerPerDevice(IConfiguration config) {
+        // For unittests, mMainConfiguration might not have been set.
+        if (config == null) {
+            return new LinkedHashMap<String, List<ITargetPreparer>>();
+        }
+        // Read the list of allowed suite level target preparers from resource files.
+        Set<String> allowedSuitePreparers = new HashSet<>();
+        for (String resource : ALLOWED_PREPARERS_CONFIGS) {
+            try (InputStream resStream = ITestSuite.class.getResourceAsStream(resource)) {
+                if (resStream == null) {
+                    CLog.d("Resource not found for allowed preparers: %s", resource);
+                    continue;
+                }
+                List<String> preparers =
+                        Arrays.asList(StreamUtil.getStringFromStream(resStream).split("\n"));
+                allowedSuitePreparers.addAll(preparers);
+            } catch (IOException e) {
+                CLog.e(e);
+            }
+        }
+
+        Map<String, List<ITargetPreparer>> res = new LinkedHashMap<>();
+        for (IDeviceConfiguration holder : config.getDeviceConfig()) {
+            List<ITargetPreparer> preparers = new ArrayList<>();
+            for (ITargetPreparer preparer : holder.getTargetPreparers()) {
+                if (allowedSuitePreparers.contains(preparer.getClass().getCanonicalName()))
+                    preparers.add(preparer);
+            }
+            res.put(holder.getDeviceName(), preparers);
+        }
+        return res;
+    }
+
     /**
      * Opportunity to clean up all the things that were needed during the suites setup but are not
      * required to run the tests.
@@ -948,6 +993,7 @@
                     ModuleSplitter.splitConfiguration(
                             testInfo,
                             runConfig,
+                            getAllowedPreparerPerDevice(mMainConfiguration),
                             shardCountHint,
                             mShouldMakeDynamicModule,
                             mIntraModuleSharding);
diff --git a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
index 0372eb7..2157877 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
@@ -59,6 +59,7 @@
 import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.result.error.ErrorIdentifier;
 import com.android.tradefed.result.error.InfraErrorIdentifier;
+import com.android.tradefed.result.error.TestErrorIdentifier;
 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.retry.IRetryDecision;
 import com.android.tradefed.retry.RetryStatistics;
@@ -125,10 +126,13 @@
     private IConfiguration mInternalTestConfiguration;
     private IConfiguration mInternalTargetPreparerConfiguration;
     private ILogSaver mLogSaver;
+    private TestInformation mModuleInfo;
+    private ITestInvocationListener mInvocationListener;
 
     private final String mId;
     private Collection<IRemoteTest> mTests = null;
     private Map<String, List<ITargetPreparer>> mPreparersPerDevice = null;
+    private Map<String, List<ITargetPreparer>> mSuitePreparersPerDevice = null;
 
     private List<IMultiTargetPreparer> mMultiPreparers = new ArrayList<>();
     private IBuildInfo mBuild;
@@ -174,6 +178,24 @@
             Map<String, List<ITargetPreparer>> preparersPerDevice,
             List<IMultiTargetPreparer> multiPreparers,
             IConfiguration moduleConfig) {
+        this(name, tests, preparersPerDevice, null, multiPreparers, moduleConfig);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param name unique name of the test configuration.
+     * @param tests list of {@link IRemoteTest} that needs to run.
+     * @param preparersPerDevice list of {@link ITargetPreparer} to be used to setup the device.
+     * @param moduleConfig the {@link IConfiguration} of the underlying module config.
+     */
+    public ModuleDefinition(
+            String name,
+            Collection<IRemoteTest> tests,
+            Map<String, List<ITargetPreparer>> preparersPerDevice,
+            Map<String, List<ITargetPreparer>> suitePreparersPerDevice,
+            List<IMultiTargetPreparer> multiPreparers,
+            IConfiguration moduleConfig) {
         mId = name;
         mTests = tests;
         mModuleConfiguration = moduleConfig;
@@ -204,6 +226,7 @@
 
         mMultiPreparers.addAll(multiPreparers);
         mPreparersPerDevice = preparersPerDevice;
+        mSuitePreparersPerDevice = suitePreparersPerDevice;
 
         // Get the tokens of the module
         List<String> tokens = configDescriptor.getMetaData(ITestSuite.TOKEN_KEY);
@@ -343,6 +366,9 @@
             TestFailureListener failureListener,
             int maxRunLimit)
             throws DeviceNotAvailableException {
+        mModuleInfo = moduleInfo;
+        mInvocationListener = listener;
+
         mStartModuleRunDate = System.currentTimeMillis();
         // Load extra configuration for the module from module_controller
         // TODO: make module_controller a full TF object
@@ -390,22 +416,10 @@
                             moduleInfo.getDevice(), mInternalTargetPreparerConfiguration);
         }
         // Setup
-        long prepStartTime = getCurrentTime();
         if (preparationException == null) {
-            preparationException = runTargetPreparation(moduleInfo, listener);
+            preparationException = runPreparation(false);
         }
-        // Skip multi-preparation if preparation already failed.
-        if (preparationException == null) {
-            for (IMultiTargetPreparer multiPreparer : mMultiPreparers) {
-                preparationException = runMultiPreparerSetup(multiPreparer, moduleInfo, listener);
-                if (preparationException != null) {
-                    mIsFailedModule = true;
-                    CLog.e("Some preparation step failed. failing the module %s", getId());
-                    break;
-                }
-            }
-        }
-        mElapsedPreparation = getCurrentTime() - prepStartTime;
+
         // Run the tests
         try {
             if (preparationException != null) {
@@ -509,7 +523,13 @@
                 // After the run, if the test failed (even after retry the final result passed) has
                 // failed, capture a bugreport.
                 if (retriableTest.getResultListener().hasLastAttemptFailed()) {
-                    captureBugreport(listener, getId());
+                    captureBugreport(
+                            listener,
+                            getId(),
+                            retriableTest
+                                    .getResultListener()
+                                    .getCurrentRunResults()
+                                    .getRunFailureDescription());
                 }
             }
         } finally {
@@ -598,7 +618,7 @@
             int maxRunLimit) {
         GranularRetriableTestWrapper retriableTest =
                 new GranularRetriableTestWrapper(
-                        test, listener, failureListener, moduleLevelListeners, maxRunLimit);
+                        test, this, listener, failureListener, moduleLevelListeners, maxRunLimit);
         retriableTest.setModuleId(getId());
         retriableTest.setMarkTestsSkipped(skipTestCases);
         retriableTest.setMetricCollectors(mRunMetricCollectors);
@@ -609,7 +629,13 @@
         return retriableTest;
     }
 
-    private void captureBugreport(ITestLogger listener, String moduleId) {
+    private void captureBugreport(
+            ITestLogger listener, String moduleId, FailureDescription failure) {
+        FailureStatus status = failure.getFailureStatus();
+        if (!FailureStatus.LOST_SYSTEM_UNDER_TEST.equals(status)
+                && !FailureStatus.SYSTEM_UNDER_TEST_CRASHED.equals(status)) {
+            return;
+        }
         for (ITestDevice device : mModuleInvocationContext.getDevices()) {
             if (device.getIDevice() instanceof StubDevice) {
                 continue;
@@ -735,7 +761,7 @@
             listener.testStarted(testEntry.getKey(), testEntry.getValue().getStartTime());
             switch (testEntry.getValue().getStatus()) {
                 case FAILURE:
-                    listener.testFailed(testEntry.getKey(), testEntry.getValue().getStackTrace());
+                    listener.testFailed(testEntry.getKey(), testEntry.getValue().getFailure());
                     break;
                 case ASSUMPTION_FAILURE:
                     listener.testAssumptionFailure(
@@ -746,7 +772,10 @@
                     break;
                 case INCOMPLETE:
                     listener.testFailed(
-                            testEntry.getKey(), "Test did not complete due to exception.");
+                            testEntry.getKey(),
+                            FailureDescription.create(
+                                    "Test did not complete due to exception.",
+                                    FailureStatus.TEST_FAILURE));
                     break;
                 default:
                     break;
@@ -766,11 +795,41 @@
         }
     }
 
+    /**
+     * Run preparers of the test, including suite level preparers if specified.
+     *
+     * @param includeSuitePreparers Set to {@code true} to also run suite level preparers.
+     * @return {@link Throwable} of any exception raised when running preparers.
+     */
+    public Throwable runPreparation(boolean includeSuitePreparers) {
+        Throwable preparationException = null;
+        long prepStartTime = getCurrentTime();
+        if (includeSuitePreparers) {
+            // Run suite level preparers.
+            preparationException = runTargetPreparation(mSuitePreparersPerDevice);
+        }
+
+        if (preparationException == null) {
+            preparationException = runTargetPreparation(mPreparersPerDevice);
+        }
+        // Skip multi-preparation if preparation already failed.
+        if (preparationException == null) {
+            for (IMultiTargetPreparer multiPreparer : mMultiPreparers) {
+                preparationException = runMultiPreparerSetup(multiPreparer);
+                if (preparationException != null) {
+                    mIsFailedModule = true;
+                    CLog.e("Some preparation step failed. failing the module %s", getId());
+                    break;
+                }
+            }
+        }
+        mElapsedPreparation = getCurrentTime() - prepStartTime;
+        return preparationException;
+    }
+
     /** Run all the prepare steps. */
     private Throwable runPreparerSetup(
-            TestInformation moduleInfo,
             ITargetPreparer preparer,
-            ITestLogger logger,
             int deviceIndex) {
         if (preparer.isDisabled()) {
             // If disabled skip completely.
@@ -781,14 +840,14 @@
         try {
             // set the logger in case they need it.
             if (preparer instanceof ITestLoggerReceiver) {
-                ((ITestLoggerReceiver) preparer).setTestLogger(logger);
+                ((ITestLoggerReceiver) preparer).setTestLogger(mInvocationListener);
             }
             if (preparer instanceof IInvocationContextReceiver) {
                 ((IInvocationContextReceiver) preparer)
                         .setInvocationContext(mModuleInvocationContext);
             }
-            moduleInfo.setActiveDeviceIndex(deviceIndex);
-            preparer.setUp(moduleInfo);
+            mModuleInfo.setActiveDeviceIndex(deviceIndex);
+            preparer.setUp(mModuleInfo);
             return null;
         } catch (BuildError
                 | TargetSetupError
@@ -802,13 +861,12 @@
             CLog.e(e);
             return e;
         } finally {
-            moduleInfo.setActiveDeviceIndex(0);
+            mModuleInfo.setActiveDeviceIndex(0);
         }
     }
 
     /** Run all multi target preparer step. */
-    private Throwable runMultiPreparerSetup(
-            IMultiTargetPreparer preparer, TestInformation moduleInfo, ITestLogger logger) {
+    private Throwable runMultiPreparerSetup(IMultiTargetPreparer preparer) {
         if (preparer.isDisabled()) {
             // If disabled skip completely.
             return null;
@@ -818,13 +876,13 @@
         try {
             // set the logger in case they need it.
             if (preparer instanceof ITestLoggerReceiver) {
-                ((ITestLoggerReceiver) preparer).setTestLogger(logger);
+                ((ITestLoggerReceiver) preparer).setTestLogger(mInvocationListener);
             }
             if (preparer instanceof IInvocationContextReceiver) {
                 ((IInvocationContextReceiver) preparer)
                         .setInvocationContext(mModuleInvocationContext);
             }
-            preparer.setUp(moduleInfo);
+            preparer.setUp(mModuleInfo);
             return null;
         } catch (BuildError
                 | TargetSetupError
@@ -976,6 +1034,14 @@
     }
 
     /**
+     * Returns the list of suite level {@link ITargetPreparer} associated with the given device name
+     */
+    @VisibleForTesting
+    List<ITargetPreparer> getSuitePreparerForDevice(String deviceName) {
+        return mSuitePreparersPerDevice.get(deviceName);
+    }
+
+    /**
      * When running unit tests for ModuleDefinition we don't want to unnecessarily report some auto
      * retry times.
      */
@@ -996,7 +1062,9 @@
         }
         listener.testRunStarted(getId(), 0, 0, System.currentTimeMillis());
         FailureDescription description =
-                FailureDescription.create(message).setFailureStatus(FailureStatus.NOT_EXECUTED);
+                FailureDescription.create(message)
+                        .setFailureStatus(FailureStatus.NOT_EXECUTED)
+                        .setErrorIdentifier(TestErrorIdentifier.MODULE_DID_NOT_EXECUTE);
         listener.testRunFailed(description);
         listener.testRunEnded(0, new HashMap<String, Metric>());
         listener.testModuleEnded();
@@ -1044,28 +1112,28 @@
                 InvocationMetricKey.AUTO_RETRY_TIME, retryTimeMs);
     }
 
-    private Throwable runTargetPreparation(TestInformation moduleInfo, ITestLogger logger) {
+    private Throwable runTargetPreparation(Map<String, List<ITargetPreparer>> preparersPerDevice) {
         Throwable preparationException = null;
         for (int i = 0; i < mModuleInvocationContext.getDeviceConfigNames().size(); i++) {
             String deviceName = mModuleInvocationContext.getDeviceConfigNames().get(i);
-            if (i >= mPreparersPerDevice.size()) {
+            if (i >= preparersPerDevice.size()) {
                 CLog.d(
                         "Main configuration has more devices than the module configuration. '%s' "
                                 + "will not run any preparation.",
                         deviceName);
                 continue;
             }
-            List<ITargetPreparer> preparers = mPreparersPerDevice.get(deviceName);
+            List<ITargetPreparer> preparers = preparersPerDevice.get(deviceName);
             if (preparers == null) {
                 CLog.w(
                         "Module configuration devices mismatch the main configuration "
                                 + "(Missing device '%s'), resolving preparers by index.",
                         deviceName);
-                String key = new ArrayList<>(mPreparersPerDevice.keySet()).get(i);
-                preparers = mPreparersPerDevice.get(key);
+                String key = new ArrayList<>(preparersPerDevice.keySet()).get(i);
+                preparers = preparersPerDevice.get(key);
             }
             for (ITargetPreparer preparer : preparers) {
-                preparationException = runPreparerSetup(moduleInfo, preparer, logger, i);
+                preparationException = runPreparerSetup(preparer, i);
                 if (preparationException != null) {
                     mIsFailedModule = true;
                     CLog.e("Some preparation step failed. failing the module %s", getId());
@@ -1123,6 +1191,11 @@
             failureDescription.setErrorIdentifier(id);
             failureDescription.setFailureStatus(id.status());
             failureDescription.setOrigin(((IHarnessException) setupException).getOrigin());
+        } else if (setupException instanceof RuntimeException) {
+            // TODO: switch to customer_issue
+            failureDescription.setFailureStatus(FailureStatus.UNSET);
+            failureDescription.setErrorIdentifier(
+                    InfraErrorIdentifier.MODULE_SETUP_RUNTIME_EXCEPTION);
         } else {
             failureDescription.setFailureStatus(FailureStatus.UNSET);
         }
diff --git a/src/com/android/tradefed/testtype/suite/ModuleSplitter.java b/src/com/android/tradefed/testtype/suite/ModuleSplitter.java
index a4fa85f..970522a 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleSplitter.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleSplitter.java
@@ -63,6 +63,7 @@
      *
      * @param testInfo the current {@link TestInformation} to proceed with sharding.
      * @param runConfig {@link LinkedHashMap} loaded from {@link ITestSuite#loadTests()}.
+     * @param suitePreparersPerDevice map of suite level preparers per test device.
      * @param shardCount a shard count hint to help with sharding.
      * @param dynamicModule Whether or not module can be shared in pool or must be independent
      *     (strict sharding).
@@ -72,6 +73,7 @@
     public static List<ModuleDefinition> splitConfiguration(
             TestInformation testInfo,
             LinkedHashMap<String, IConfiguration> runConfig,
+            Map<String, List<ITargetPreparer>> suitePreparersPerDevice,
             int shardCount,
             boolean dynamicModule,
             boolean intraModuleSharding) {
@@ -93,7 +95,8 @@
                         configMap.getValue(),
                         shardCount,
                         dynamicModule,
-                        intraModuleSharding);
+                        intraModuleSharding,
+                        suitePreparersPerDevice);
             } catch (RuntimeException e) {
                 CLog.e("Exception while creating module for '%s'", configMap.getKey());
                 throw e;
@@ -109,7 +112,8 @@
             IConfiguration config,
             int shardCount,
             boolean dynamicModule,
-            boolean intraModuleSharding) {
+            boolean intraModuleSharding,
+            Map<String, List<ITargetPreparer>> suitePreparersPerDevice) {
         List<IRemoteTest> tests = config.getTests();
         // Get rid of the IRemoteTest reference on the shared configuration. It will not be used
         // to run.
@@ -127,11 +131,13 @@
                                     moduleName,
                                     tests,
                                     clonePreparersMap(config),
+                                    clonePreparersMap(suitePreparersPerDevice),
                                     clonePreparers(config.getMultiTargetPreparers()),
                                     config);
                     currentList.add(module);
                 } else {
-                    addModuleToListFromSingleTest(currentList, tests.get(i), moduleName, config);
+                    addModuleToListFromSingleTest(
+                            currentList, tests.get(i), moduleName, config, suitePreparersPerDevice);
                 }
             }
             clearPreparersFromConfig(config);
@@ -153,6 +159,7 @@
                                             moduleName,
                                             shardedTests,
                                             clonePreparersMap(config),
+                                            clonePreparersMap(suitePreparersPerDevice),
                                             clonePreparers(config.getMultiTargetPreparers()),
                                             config);
                             currentList.add(module);
@@ -161,14 +168,19 @@
                         // We create independent modules with each sharded test.
                         for (IRemoteTest moduleTest : shardedTests) {
                             addModuleToListFromSingleTest(
-                                    currentList, moduleTest, moduleName, config);
+                                    currentList,
+                                    moduleTest,
+                                    moduleName,
+                                    config,
+                                    suitePreparersPerDevice);
                         }
                     }
                     continue;
                 }
             }
             // test is not shardable or did not shard
-            addModuleToListFromSingleTest(currentList, test, moduleName, config);
+            addModuleToListFromSingleTest(
+                    currentList, test, moduleName, config, suitePreparersPerDevice);
         }
         clearPreparersFromConfig(config);
     }
@@ -181,7 +193,8 @@
             List<ModuleDefinition> currentList,
             IRemoteTest test,
             String moduleName,
-            IConfiguration config) {
+            IConfiguration config,
+            Map<String, List<ITargetPreparer>> suitePreparersPerDevice) {
         List<IRemoteTest> testList = new ArrayList<>();
         testList.add(test);
         ModuleDefinition module =
@@ -189,6 +202,7 @@
                         moduleName,
                         testList,
                         clonePreparersMap(config),
+                        clonePreparersMap(suitePreparersPerDevice),
                         clonePreparers(config.getMultiTargetPreparers()),
                         config);
         currentList.add(module);
@@ -234,6 +248,18 @@
         return res;
     }
 
+    /** Deep cloning of potentially multi-device preparers. */
+    private static Map<String, List<ITargetPreparer>> clonePreparersMap(
+            Map<String, List<ITargetPreparer>> suitePreparersPerDevice) {
+        Map<String, List<ITargetPreparer>> res = new LinkedHashMap<>();
+        for (String device : suitePreparersPerDevice.keySet()) {
+            List<ITargetPreparer> preparers = new ArrayList<>();
+            res.put(device, preparers);
+            preparers.addAll(clonePreparers(suitePreparersPerDevice.get(device)));
+        }
+        return res;
+    }
+
     private static void clearPreparersFromConfig(IConfiguration config) {
         try {
             for (IDeviceConfiguration holder : config.getDeviceConfig()) {
diff --git a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
index 26a7f4a..2831e8b 100644
--- a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
+++ b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
@@ -79,6 +79,7 @@
 
     private boolean mAllowParameterizedModules = false;
     private boolean mAllowMainlineParameterizedModules = false;
+    private boolean mOptimizeMainlineTest = false;
     private boolean mAllowOptionalParameterizedModules = false;
     private ModuleParameters mForcedModuleParameter = null;
     private Set<ModuleParameters> mExcludedModuleParameters = new HashSet<>();
@@ -121,6 +122,11 @@
         mAllowMainlineParameterizedModules = allowed;
     }
 
+    /** Sets whether or not to optimize mainline test. */
+    public final void setOptimizeMainlineTest(boolean allowed) {
+        mOptimizeMainlineTest = allowed;
+    }
+
     /** Sets whether or not to allow optional parameterized modules. */
     public final void setOptionalParameterizedModules(boolean allowed) {
         mAllowOptionalParameterizedModules = allowed;
@@ -378,7 +384,8 @@
                                 new MainlineModuleHandler(
                                         param,
                                         abi,
-                                        mContext
+                                        mContext,
+                                        mOptimizeMainlineTest
                                 );
                         skipCreatingBaseConfig = true;
                         IConfiguration paramConfig =
diff --git a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
index da1741f..6082517 100644
--- a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
+++ b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
@@ -219,9 +219,9 @@
         if (configPath == null) {
             throw new RuntimeException(String.format("Configuration path is null."));
         }
-        File configFie = new File(configPath);
-        if (!configFie.exists()) {
-            configFie = null;
+        File configFile = new File(configPath);
+        if (!configFile.exists()) {
+            configFile = null;
         }
         // De-duplicate test infos so that there won't be duplicate test options.
         testInfos = dedupTestInfos(testInfos);
@@ -229,10 +229,10 @@
             // Clean up all the test options injected in SuiteModuleLoader.
             super.cleanUpSuiteSetup();
             super.clearModuleArgs();
-            if (configFie != null) {
+            if (configFile != null) {
                 clearConfigPaths();
                 // Set config path to BaseTestSuite to limit the search.
-                addConfigPaths(configFie);
+                addConfigPaths(configFile);
             }
             // Inject the test options from each test info to SuiteModuleLoader.
             parseOptions(testInfo);
diff --git a/src/com/android/tradefed/testtype/suite/module/Sdk30ModuleController.java b/src/com/android/tradefed/testtype/suite/module/Sdk30ModuleController.java
new file mode 100644
index 0000000..13ca021
--- /dev/null
+++ b/src/com/android/tradefed/testtype/suite/module/Sdk30ModuleController.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.suite.module;
+
+/**
+ * Only run tests if the device under test is SDK version 30 or above.
+ *
+ * <p>Use by adding this line to your AndroidTest.xml: <object type="module_controller"
+ * class="com.android.tradefed.testtype.suite.module.Sdk30ModuleController" />
+ */
+public class Sdk30ModuleController extends MinSdkModuleController {
+    public Sdk30ModuleController() {
+        super(30);
+    }
+}
diff --git a/src/com/android/tradefed/testtype/suite/params/MainlineModuleHandler.java b/src/com/android/tradefed/testtype/suite/params/MainlineModuleHandler.java
index eb4f4a4..9f4af97 100644
--- a/src/com/android/tradefed/testtype/suite/params/MainlineModuleHandler.java
+++ b/src/com/android/tradefed/testtype/suite/params/MainlineModuleHandler.java
@@ -38,11 +38,17 @@
     private String mDynamicBaseLink = null;
     private IAbi mAbi = null;
     private String mName = null;
+    private boolean mOptimizeMainlineTest = false;
 
-    public MainlineModuleHandler(String name, IAbi abi, IInvocationContext context) {
+    public MainlineModuleHandler(
+            String name,
+            IAbi abi,
+            IInvocationContext context,
+            boolean optimize) {
         mName = name;
         mAbi = abi;
         buildDynamicBaseLink(context.getBuildInfos().get(0));
+        mOptimizeMainlineTest = optimize;
     }
 
     /** Builds the dynamic base link where the mainline modules would be downloaded. */
@@ -78,6 +84,7 @@
     private InstallApexModuleTargetPreparer createMainlineModuleInstaller() {
         InstallApexModuleTargetPreparer mainlineModuleInstaller =
                 new InstallApexModuleTargetPreparer();
+        mainlineModuleInstaller.setSkipApexTearDown(mOptimizeMainlineTest);
         // Inject the real dynamic link to the target preparer so that it will dynamically download
         // the mainline modules.
         String fullDynamicLink = mDynamicBaseLink;
diff --git a/src/com/android/tradefed/util/AaptParser.java b/src/com/android/tradefed/util/AaptParser.java
index a88fb8f..a61d230 100644
--- a/src/com/android/tradefed/util/AaptParser.java
+++ b/src/com/android/tradefed/util/AaptParser.java
@@ -54,6 +54,48 @@
     private static final int AAPT_TIMEOUT_MS = 60000;
     private static final int INVALID_SDK = -1;
 
+    /**
+     * Enum of options for AAPT version used to parse APK files.
+     */
+    public static enum AaptVersion {
+        AAPT {
+            @Override
+            public String[] dumpBadgingCommand(File apkFile) {
+                return new String[] {"aapt", "dump", "badging", apkFile.getAbsolutePath()};
+            }
+
+            @Override
+            public String[] dumpXmlTreeCommand(File apkFile) {
+                return new String[] {
+                    "aapt", "dump", "xmltree", apkFile.getAbsolutePath(), "AndroidManifest.xml"
+                };
+            }
+        },
+
+        AAPT2 {
+            @Override
+            public String[] dumpBadgingCommand(File apkFile) {
+                return new String[] {"aapt2", "dump", "badging", apkFile.getAbsolutePath()};
+            }
+
+            @Override
+            public String[] dumpXmlTreeCommand(File apkFile) {
+                return new String[] {
+                    "aapt2",
+                    "dump",
+                    "xmltree",
+                    apkFile.getAbsolutePath(),
+                    "--file",
+                    "AndroidManifest.xml"
+                };
+            }
+        };
+
+        public abstract String[] dumpBadgingCommand(File apkFile);
+
+        public abstract String[] dumpXmlTreeCommand(File apkFile);
+    };
+
     private String mPackageName;
     private String mVersionCode;
     private String mVersionName;
@@ -130,16 +172,21 @@
      * @return the {@link AaptParser} or <code>null</code> if failed to extract the information
      */
     public static AaptParser parse(File apkFile) {
+        return parse(apkFile, AaptVersion.AAPT);
+    }
+
+    /**
+     * Parse info from the apk.
+     *
+     * @param apkFile the apk file
+     * @param aaptVersion the aapt version
+     * @return the {@link AaptParser} or <code>null</code> if failed to extract the information
+     */
+    public static AaptParser parse(File apkFile, AaptVersion aaptVersion) {
         CommandResult result =
                 RunUtil.getDefault()
                         .runTimedCmdRetry(
-                                AAPT_TIMEOUT_MS,
-                                0L,
-                                2,
-                                "aapt",
-                                "dump",
-                                "badging",
-                                apkFile.getAbsolutePath());
+                                AAPT_TIMEOUT_MS, 0L, 2, aaptVersion.dumpBadgingCommand(apkFile));
 
         String stderr = result.getStderr();
         if (stderr != null && !stderr.isEmpty()) {
@@ -158,11 +205,7 @@
                                 AAPT_TIMEOUT_MS,
                                 0L,
                                 2,
-                                "aapt",
-                                "dump",
-                                "xmltree",
-                                apkFile.getAbsolutePath(),
-                                "AndroidManifest.xml");
+                                aaptVersion.dumpXmlTreeCommand(apkFile));
 
         stderr = result.getStderr();
         if (stderr != null && !stderr.isEmpty()) {
diff --git a/src/com/android/tradefed/util/AdbRootElevator.java b/src/com/android/tradefed/util/AdbRootElevator.java
new file mode 100644
index 0000000..fa6eb0c
--- /dev/null
+++ b/src/com/android/tradefed/util/AdbRootElevator.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.util;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.INativeDevice;
+
+/**
+ * An {@link java.lang.AutoCloseable} that enables adb root when constructed if needed and restores
+ * root state when complete.
+ */
+public class AdbRootElevator implements AutoCloseable {
+    private final boolean mWasRoot;
+    private final INativeDevice mDevice;
+
+    public AdbRootElevator(INativeDevice device) throws DeviceNotAvailableException {
+        mDevice = device;
+        mWasRoot = mDevice.isAdbRoot();
+        if (!mWasRoot) {
+            if (!mDevice.enableAdbRoot()) {
+                throw new RuntimeException("Failed to enable adb root.");
+            }
+        }
+    }
+
+    @Override
+    public void close() {
+        if (!mWasRoot) {
+            try {
+                mDevice.disableAdbRoot();
+            } catch (DeviceNotAvailableException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+}
diff --git a/src/com/android/tradefed/util/GCSFileDownloader.java b/src/com/android/tradefed/util/GCSFileDownloader.java
index 1e81adb..5d039e5 100644
--- a/src/com/android/tradefed/util/GCSFileDownloader.java
+++ b/src/com/android/tradefed/util/GCSFileDownloader.java
@@ -304,8 +304,12 @@
                 }
             } while (true);
         } catch (IOException e) {
-            CLog.e("Failed to download gs://%s/%s, clean up.", bucketName, remoteFilename);
-            throw new BuildRetrievalError(e.getMessage(), e);
+            String message =
+                    String.format(
+                            "Failed to download gs://%s/%s due to: %s",
+                            bucketName, remoteFilename, e.getMessage());
+            CLog.e(message);
+            throw new BuildRetrievalError(message, e, InfraErrorIdentifier.GCS_ERROR);
         }
     }
 
diff --git a/src/com/android/tradefed/util/ProtoUtil.java b/src/com/android/tradefed/util/ProtoUtil.java
new file mode 100644
index 0000000..bf1699a
--- /dev/null
+++ b/src/com/android/tradefed/util/ProtoUtil.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util;
+
+import com.android.tradefed.log.LogUtil.CLog;
+
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Message;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** Utility methods for dealing with protobuf messages type-agnostically. */
+public class ProtoUtil {
+
+    /**
+     * Get values of a nested field reference, i.e. field_1.field_2.field_3, from a proto message as
+     * a list of strings. Returns an empty list when a field cannot be found.
+     *
+     * <p>If the field reference contains repeated fields, each instance is expanded, resulting in a
+     * list of strings.
+     *
+     * @param message The protobuf {@link Message} or object to be parsed.
+     * @param references A list of field references starting at the root of the message. e.g. if we
+     *     want to read {@code field_2} under the value of {@code field_1} in {@code
+     *     messageOrObject} the list would be {@code field1}, {@code field2}.
+     * @return A list of all the fields values referred to by the reference. If {@code references}
+     *     is empty, returns {@code message.toString()} as a list. If {@code references} is invalid,
+     *     returns an empty list.
+     */
+    public static List<String> getNestedFieldFromMessageAsStrings(
+            Message message, List<String> references) {
+        return getNestedFieldFromMessageAsStringsHelper(message, references);
+    }
+
+    /**
+     * A helper method to {@code getNestedFieldFromMessageAsStrings} where the "message" can be an
+     * object in case we reach a primitive value field during recursive parsing.
+     */
+    private static List<String> getNestedFieldFromMessageAsStringsHelper(
+            Object messageOrObject, List<String> references) {
+        if (references.isEmpty()) {
+            return Arrays.asList(String.valueOf(messageOrObject));
+        }
+        if (!(messageOrObject instanceof Message)) {
+            CLog.e(
+                    "Attempting to read field %s from object of type %s, "
+                            + "which is not a proto message.",
+                    references.get(0), messageOrObject.getClass());
+            return new ArrayList<String>();
+        }
+        Message message = (Message) messageOrObject;
+        String reference = references.get(0);
+        FieldDescriptor fieldDescriptor = message.getDescriptorForType().findFieldByName(reference);
+        if (fieldDescriptor == null) {
+            CLog.e("Could not find field %s in message %s.", reference, message);
+            return new ArrayList<String>();
+        }
+        Object fieldValue = message.getField(fieldDescriptor);
+        if (fieldValue instanceof List) {
+            return ((List<? extends Object>) fieldValue)
+                    .stream()
+                    .flatMap(
+                            v ->
+                                    getNestedFieldFromMessageAsStringsHelper(
+                                                    v, references.subList(1, references.size()))
+                                            .stream())
+                    .collect(Collectors.toList());
+        }
+        return getNestedFieldFromMessageAsStringsHelper(
+                fieldValue, references.subList(1, references.size()));
+    }
+}
diff --git a/src/com/android/tradefed/util/StringEscapeUtils.java b/src/com/android/tradefed/util/StringEscapeUtils.java
index 846c0e7..4cc5741 100644
--- a/src/com/android/tradefed/util/StringEscapeUtils.java
+++ b/src/com/android/tradefed/util/StringEscapeUtils.java
@@ -47,6 +47,15 @@
                 case '\\':
                     out.append("\\\\");
                     break;
+                case '>':
+                    out.append("\\>");
+                    break;
+                case '<':
+                    out.append("\\<");
+                    break;
+                case '|':
+                    out.append("\\|");
+                    break;
                 default:
                     out.append(ch);
                     break;
diff --git a/src/com/android/tradefed/util/SubprocessTestResultsParser.java b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
index 2a5d280..c560e6a 100644
--- a/src/com/android/tradefed/util/SubprocessTestResultsParser.java
+++ b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
@@ -80,6 +80,7 @@
 
     private TestDescription mCurrentTest = null;
     private IInvocationContext mCurrentModuleContext = null;
+    private InvocationFailedEventInfo mReportedInvocationFailedEventInfo = null;
 
     private Pattern mPattern = null;
     private Map<String, EventHandler> mHandlerMap = null;
@@ -423,6 +424,7 @@
             } else {
                 mListener.invocationFailed(ifi.mCause);
             }
+            mReportedInvocationFailedEventInfo = ifi;
         }
     }
 
@@ -653,4 +655,14 @@
     public TestDescription getCurrentTest() {
         return mCurrentTest;
     }
+
+    /** Returns whether or not an invocation failed was reported. */
+    public boolean reportedInvocationFailed() {
+        return (mReportedInvocationFailedEventInfo != null);
+    }
+
+    /** Returns reported invocation failure event info. */
+    public InvocationFailedEventInfo getReportedInvocationFailedEventInfo() {
+        return mReportedInvocationFailedEventInfo;
+    }
 }
diff --git a/src/com/android/tradefed/util/executor/ParallelDeviceExecutor.java b/src/com/android/tradefed/util/executor/ParallelDeviceExecutor.java
index c176bdb..7a94bf5 100644
--- a/src/com/android/tradefed/util/executor/ParallelDeviceExecutor.java
+++ b/src/com/android/tradefed/util/executor/ParallelDeviceExecutor.java
@@ -15,28 +15,27 @@
  */
 package com.android.tradefed.util.executor;
 
-import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
 
-/** Wrapper of {@link ExecutorService} to execute a function on all devices in parallel. */
+/** Wrapper of {@link ExecutorService} to execute a function in parallel. */
 public class ParallelDeviceExecutor<V> {
 
-    private List<ITestDevice> mDevices;
+    private final int mPoolSize;
     private List<Throwable> mErrors;
 
-    public ParallelDeviceExecutor(List<ITestDevice> devices) {
-        mDevices = devices;
+    public ParallelDeviceExecutor(int poolSize) {
+        mPoolSize = poolSize;
         mErrors = new ArrayList<>();
     }
 
@@ -44,14 +43,14 @@
      * Invoke all the {@link Callable} with the timeout limit.
      *
      * @param callableTasks The List of tasks.
-     * @param timeout The timeout to apply.
+     * @param timeout The timeout to apply, or zero for unlimited.
      * @param unit The unit of the timeout.
      * @return The list of results for each callable task.
      */
     public List<V> invokeAll(List<Callable<V>> callableTasks, long timeout, TimeUnit unit) {
         ExecutorService executor =
                 Executors.newFixedThreadPool(
-                        mDevices.size(),
+                        mPoolSize,
                         new ThreadFactory() {
                             @Override
                             public Thread newThread(Runnable r) {
@@ -62,12 +61,15 @@
                         });
         List<V> results = new ArrayList<>();
         try {
-            List<Future<V>> futures = executor.invokeAll(callableTasks);
+            List<Future<V>> futures =
+                    timeout == 0L
+                            ? executor.invokeAll(callableTasks)
+                            : executor.invokeAll(callableTasks, timeout, unit);
             for (Future<V> future : futures) {
                 try {
-                    results.add(future.get(timeout, unit));
-                } catch (TimeoutException timeoutException) {
-                    mErrors.add(timeoutException);
+                    results.add(future.get());
+                } catch (CancellationException cancellationException) {
+                    mErrors.add(cancellationException);
                 } catch (ExecutionException execException) {
                     mErrors.add(execException.getCause());
                 }
diff --git a/test_framework/com/android/tradefed/device/metric/AtraceCollector.java b/test_framework/com/android/tradefed/device/metric/AtraceCollector.java
index c6ea63b..5a32035 100644
--- a/test_framework/com/android/tradefed/device/metric/AtraceCollector.java
+++ b/test_framework/com/android/tradefed/device/metric/AtraceCollector.java
@@ -27,6 +27,7 @@
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.IRunUtil;
@@ -268,7 +269,9 @@
                     postProcess(trace);
                     trace.delete();
                 } else {
-                    throw new DeviceRuntimeException("failed to pull log: " + fullLogPath());
+                    throw new DeviceRuntimeException(
+                            String.format("failed to pull log: %s", fullLogPath()),
+                            DeviceErrorIdentifier.FAIL_PULL_FILE);
                 }
 
                 if (!mPreserveOndeviceLog) {
diff --git a/test_framework/com/android/tradefed/device/metric/BuddyInfoMetricCollector.java b/test_framework/com/android/tradefed/device/metric/BuddyInfoMetricCollector.java
deleted file mode 100644
index 304985e..0000000
--- a/test_framework/com/android/tradefed/device/metric/BuddyInfoMetricCollector.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.google.common.io.Files;
-import java.io.File;
-import java.io.IOException;
-
-/** A {@link ScheduledDeviceMetricCollector} to collect fragmentation at regular intervals. */
-public class BuddyInfoMetricCollector extends ScheduledDeviceMetricCollector {
-    public BuddyInfoMetricCollector() {
-        setTag("fragmentation");
-    }
-
-    @Override
-    void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        try {
-            CLog.i("Running unusable-index collector...");
-            String outputFileName =
-                    String.format("%s/unusable-index-%s", createTempDir(), getFileSuffix());
-            File outputFile =
-                    saveProcessOutput(device, "cat /d/extfrag/unusable_index", outputFileName);
-            try (InputStreamSource source = new FileInputStreamSource(outputFile, true)) {
-                getInvocationListener()
-                        .testLog(
-                                Files.getNameWithoutExtension(outputFile.getName()),
-                                LogDataType.TEXT,
-                                source);
-            }
-
-        } catch (DeviceNotAvailableException | IOException e) {
-            CLog.e(e);
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/BugreportzMetricCollector.java b/test_framework/com/android/tradefed/device/metric/BugreportzMetricCollector.java
deleted file mode 100644
index 8bf0dd6..0000000
--- a/test_framework/com/android/tradefed/device/metric/BugreportzMetricCollector.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device.metric;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-
-/** A {@link ScheduledDeviceMetricCollector} to collect zipped bugreport at regular intervals. */
-public class BugreportzMetricCollector extends ScheduledDeviceMetricCollector {
-    public BugreportzMetricCollector() {
-        setTag("bugreportz");
-    }
-
-    @Override
-    void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        CLog.i("Running bugreportz...");
-
-        String hostBugreportFilename = String.format("bugreport-%s", getFileSuffix());
-        if (!device.logBugreport(hostBugreportFilename, getInvocationListener())) {
-            CLog.e("Failed to run bugreportz or bugreport.");
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/DumpHeapCollector.java b/test_framework/com/android/tradefed/device/metric/DumpHeapCollector.java
deleted file mode 100644
index 404580a..0000000
--- a/test_framework/com/android/tradefed/device/metric/DumpHeapCollector.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device.metric;
-
-import com.android.annotations.VisibleForTesting;
-import com.android.loganalysis.item.CompactMemInfoItem;
-import com.android.loganalysis.parser.CompactMemInfoParser;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.util.FileUtil;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * A {@link ScheduledDeviceMetricCollector} to collect memory dumps of processes at regular
- * intervals.
- */
-public class DumpHeapCollector extends ScheduledDeviceMetricCollector {
-    private static final String DUMPHEAP_OUTPUT = "/data/local/tmp";
-    private static final String SUFFIX = "trigger";
-
-    @Option(
-        name = "dumpheap-thresholds",
-        description =
-                "Threshold map for taking process dumpheaps. "
-                        + "The key should be the process name and its corresponding value is the "
-                        + "maximum acceptable heap size for that process."
-                        + "Note that to get heap dump for native and managed processes set their "
-                        + "threshold to 0."
-    )
-    protected Map<String, Long> mDumpheapThresholds = new HashMap<String, Long>();
-
-    @Override
-    public void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        CLog.i("Running dumpheap collection...");
-        List<File> dumpFiles = new ArrayList<>();
-        try {
-            for (String process : mDumpheapThresholds.keySet()) {
-                String output =
-                        device.executeShellCommand(
-                                String.format("dumpsys meminfo -c | grep %s", process));
-
-                dumpFiles = takeDumpheap(device, output, process, mDumpheapThresholds.get(process));
-
-                dumpFiles.forEach(dumpheap -> saveDumpheap(dumpheap));
-            }
-        } catch (DeviceNotAvailableException e) {
-            CLog.e(e);
-        } finally {
-            dumpFiles.forEach(dumpFile -> FileUtil.deleteFile(dumpFile));
-        }
-    }
-
-    /**
-     * Collects heap dump for each requested process if the PSS is greater than a threshold.
-     *
-     * @param device
-     * @param output of the meminfo command.
-     * @param process for which we need the heap dump.
-     * @param threshold which is the maximum tolerable PSS.
-     * @return the list of {@link File}s in the host containing the report. Empty list if something
-     *     failed.
-     * @throws DeviceNotAvailableException
-     */
-    @VisibleForTesting
-    List<File> takeDumpheap(ITestDevice device, String output, String process, Long threshold)
-            throws DeviceNotAvailableException {
-        List<File> dumpFiles = new ArrayList<>();
-        if (output.isEmpty()) {
-            CLog.i("Skipping %s -- no process found.", process);
-            return dumpFiles;
-        }
-
-        CompactMemInfoItem item =
-                new CompactMemInfoParser().parse(Arrays.asList(output.split("\n")));
-
-        for (Integer pid : item.getPids()) {
-            if (item.getName(pid).equals(process) && item.getPss(pid) > threshold) {
-                File dump = device.dumpHeap(process, getDevicePath(process));
-                dumpFiles.add(dump);
-            }
-        }
-        return dumpFiles;
-    }
-
-    /**
-     * Returns the path on the device to put the dump.
-     *
-     * @param process for which dump is being requested.
-     * @return a write-able path in device.
-     */
-    private String getDevicePath(String process) {
-        return String.format(
-                "%s/%s_%s_%s.hprof", DUMPHEAP_OUTPUT, process, SUFFIX, getFileSuffix());
-    }
-
-    @VisibleForTesting
-    void saveDumpheap(File dumpheap) {
-        if (dumpheap == null) {
-            CLog.e("Failed to take dumpheap.");
-            return;
-        }
-        try (FileInputStreamSource stream = new FileInputStreamSource(dumpheap)) {
-            getInvocationListener()
-                    .testLog(FileUtil.getBaseName(dumpheap.getName()), LogDataType.HPROF, stream);
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/GraphicsStatsMetricCollector.java b/test_framework/com/android/tradefed/device/metric/GraphicsStatsMetricCollector.java
deleted file mode 100644
index 8e73eda..0000000
--- a/test_framework/com/android/tradefed/device/metric/GraphicsStatsMetricCollector.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.google.common.io.Files;
-import java.io.File;
-import java.io.IOException;
-
-/** A {@link ScheduledDeviceMetricCollector} to collect graphics stats at regular intervals. */
-public class GraphicsStatsMetricCollector extends ScheduledDeviceMetricCollector {
-    GraphicsStatsMetricCollector() {
-        setTag("jank");
-    }
-
-    @Override
-    public void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        try {
-            CLog.i("Running graphicsstats...");
-            String outputFileName =
-                    String.format("%s/graphics-%s", createTempDir(), getFileSuffix());
-            File outputFile = saveProcessOutput(device, "dumpsys graphicsstats", outputFileName);
-            try (InputStreamSource source = new FileInputStreamSource(outputFile, true)) {
-                getInvocationListener()
-                        .testLog(
-                                Files.getNameWithoutExtension(outputFile.getName()),
-                                LogDataType.GFX_INFO,
-                                source);
-            }
-        } catch (DeviceNotAvailableException | IOException e) {
-            CLog.e(e);
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/IonHeapInfoMetricCollector.java b/test_framework/com/android/tradefed/device/metric/IonHeapInfoMetricCollector.java
deleted file mode 100644
index 3312855..0000000
--- a/test_framework/com/android/tradefed/device/metric/IonHeapInfoMetricCollector.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.google.common.io.Files;
-import java.io.File;
-import java.io.IOException;
-
-/**
- * A {@link ScheduledDeviceMetricCollector} to collect audio and system memory heaps at regular
- * intervals.
- */
-public class IonHeapInfoMetricCollector extends ScheduledDeviceMetricCollector {
-    public IonHeapInfoMetricCollector() {
-        setTag("ion");
-    }
-
-    @Override
-    void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        collectIonAudio(device);
-        collectIonSystem(device);
-    }
-
-    private void collectIonAudio(ITestDevice device) {
-        try {
-            CLog.i("Running ionheap audio collector...");
-            String outputFileName =
-                    String.format("%s/ion-audio-%s", createTempDir(), getFileSuffix());
-            File outputFile = saveProcessOutput(device, "cat /d/ion/heaps/audio", outputFileName);
-            try (InputStreamSource source = new FileInputStreamSource(outputFile, true)) {
-                getInvocationListener()
-                        .testLog(
-                                Files.getNameWithoutExtension(outputFile.getName()),
-                                LogDataType.TEXT,
-                                source);
-            }
-        } catch (DeviceNotAvailableException | IOException e) {
-            CLog.e(e);
-        }
-    }
-
-    private void collectIonSystem(ITestDevice device) {
-        try {
-            CLog.i("Running ionheap system collector...");
-            String outputFileName =
-                    String.format("%s/ion-system-%s", createTempDir(), getFileSuffix());
-            File outputFile = saveProcessOutput(device, "cat /d/ion/heaps/system", outputFileName);
-            try (InputStreamSource source = new FileInputStreamSource(outputFile, true)) {
-                getInvocationListener()
-                        .testLog(
-                                Files.getNameWithoutExtension(outputFile.getName()),
-                                LogDataType.TEXT,
-                                source);
-            }
-        } catch (DeviceNotAvailableException | IOException e) {
-            CLog.e(e);
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/MemInfoMetricCollector.java b/test_framework/com/android/tradefed/device/metric/MemInfoMetricCollector.java
deleted file mode 100644
index 0890b74..0000000
--- a/test_framework/com/android/tradefed/device/metric/MemInfoMetricCollector.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.google.common.io.Files;
-import java.io.File;
-import java.io.IOException;
-
-/** A {@link ScheduledDeviceMetricCollector} to collect memory dumps at regular intervals. */
-public class MemInfoMetricCollector extends ScheduledDeviceMetricCollector {
-    MemInfoMetricCollector() {
-        setTag("compact-meminfo");
-    }
-
-    @Override
-    public void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        try {
-            CLog.i("Running meminfo...");
-            String outputFileName =
-                    String.format("%s/compact-meminfo-%s", createTempDir(), getFileSuffix());
-            File outputFile = saveProcessOutput(device, "dumpsys meminfo -c -S", outputFileName);
-            try (InputStreamSource source = new FileInputStreamSource(outputFile, true)) {
-                getInvocationListener()
-                        .testLog(
-                                Files.getNameWithoutExtension(outputFile.getName()),
-                                LogDataType.COMPACT_MEMINFO,
-                                source);
-            }
-        } catch (DeviceNotAvailableException | IOException e) {
-            CLog.e(e);
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/PagetypeInfoMetricCollector.java b/test_framework/com/android/tradefed/device/metric/PagetypeInfoMetricCollector.java
deleted file mode 100644
index 9ab0f33..0000000
--- a/test_framework/com/android/tradefed/device/metric/PagetypeInfoMetricCollector.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.google.common.io.Files;
-import java.io.File;
-import java.io.IOException;
-
-/** A {@link ScheduledDeviceMetricCollector} to collect free page counts at regular intervals. */
-public class PagetypeInfoMetricCollector extends ScheduledDeviceMetricCollector {
-    public PagetypeInfoMetricCollector() {
-        setTag("pagetypeinfo");
-    }
-
-    @Override
-    void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        try {
-            CLog.i("Running pagetype info collector...");
-            String outputFileName =
-                    String.format("%s/pagetypeinfo-%s", createTempDir(), getFileSuffix());
-            File outputFile = saveProcessOutput(device, "cat /proc/pagetypeinfo", outputFileName);
-            try (InputStreamSource source = new FileInputStreamSource(outputFile, true)) {
-                getInvocationListener()
-                        .testLog(
-                                Files.getNameWithoutExtension(outputFile.getName()),
-                                LogDataType.TEXT,
-                                source);
-            }
-        } catch (DeviceNotAvailableException | IOException e) {
-            CLog.e(e);
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java b/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
index 257e9e5..4d496a4 100644
--- a/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
+++ b/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
@@ -63,6 +63,8 @@
     private static final String EXTRACTOR_SUCCESS = "1";
     private static final String EXTRACTOR_FAILURE = "0";
     private static final String EXTRACTOR_RUNTIME = "trace_extractor_runtime";
+    private static final String RAW_TRACE_FILE_SIZE = "perfetto_trace_file_size_bytes";
+    private static final String NSS_CACHE_ERROR = "base/nsscache-inl.h failed to lookup";
 
     public enum METRIC_FILE_FORMAT {
         text,
@@ -142,6 +144,11 @@
             description = "Convert the raw trace file to perfetto metric file.")
     private boolean mConvertToMetricFile = true;
 
+    @Option(name = "collect-perfetto-file-size",
+            description = "Set it to true to collect the perfetto file size as part"
+                    + " of the metrics.")
+    private boolean mCollectPerfettoFileSize = false;
+
     @Option(
             name = "trace-processor-binary",
             description = "Path to the trace processor shell. This will"
@@ -190,6 +197,15 @@
             processSrcFile = decompressFile(metricFile);
         }
 
+        // Update the file size metrics.
+        if (processSrcFile != null && mCollectPerfettoFileSize) {
+            double perfettoFileSizeInBytes = processSrcFile.length();
+            Metric.Builder metricDurationBuilder = Metric.newBuilder();
+            metricDurationBuilder.getMeasurementsBuilder().setSingleDouble(
+                    perfettoFileSizeInBytes);
+            data.addMetric(RAW_TRACE_FILE_SIZE, metricDurationBuilder.setType(DataType.RAW));
+        }
+
         // Convert to perfetto metric format.
         if (mConvertToMetricFile) {
             File convertedMetricFile = convertToMetricProto(processSrcFile);
@@ -234,9 +250,23 @@
                         String.format("%s_%s", mMetricPrefix, EXTRACTOR_RUNTIME),
                         metricDurationBuilder.setType(DataType.RAW));
 
-                if (CommandStatus.SUCCESS.equals(cr.getStatus())) {
+                // Adding temporary workaround to handle the NSS cache error.
+                // TODO: Revert the NSS cache error handling after b/156924255 is fixed.
+                if (CommandStatus.SUCCESS.equals(cr.getStatus()) ||
+                        (CommandStatus.FAILED.equals(cr.getStatus()) &&
+                                cr.getStdout().contains(NSS_CACHE_ERROR))) {
                     String[] metrics = cr.getStdout().split(LINE_SEPARATOR);
+
+                    boolean isMetricStarted = false;
                     for (String metric : metrics) {
+                        // Skip until the first metric line is parsed.
+                        // Usually "trace-durations-ms" is the first metric from the output.
+                        if(isMetricStarted || metric.contains("trace-duration-ms")) {
+                            isMetricStarted = true;
+                        } else {
+                            continue;
+                        }
+
                         Pair<String, String> kv = splitKeyValue(metric);
 
                         if (kv != null) {
diff --git a/test_framework/com/android/tradefed/device/metric/ProcessMaxMemoryCollector.java b/test_framework/com/android/tradefed/device/metric/ProcessMaxMemoryCollector.java
deleted file mode 100644
index 5528177..0000000
--- a/test_framework/com/android/tradefed/device/metric/ProcessMaxMemoryCollector.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import com.android.loganalysis.item.DumpsysProcessMeminfoItem;
-import com.android.loganalysis.parser.DumpsysProcessMeminfoParser;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.metrics.proto.MetricMeasurement.DataType;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.metrics.proto.MetricMeasurement.NumericValues;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-
-/**
- * A {@link ScheduledDeviceMetricCollector} to measure peak memory usage of specified processes.
- * Collects PSS and USS (private dirty) memory usage values from dumpsys meminfo. The result will be
- * reported as a test run metric with key in the form of PSS#ProcName[#DeviceNum], in KB.
- */
-public class ProcessMaxMemoryCollector extends ScheduledDeviceMetricCollector {
-
-    @Option(
-        name = "memory-usage-process-name",
-        description = "Process names (from `dumpsys meminfo`) to measure memory usage for"
-    )
-    private List<String> mProcessNames = new ArrayList<>();
-
-    private class DeviceMemoryData {
-        /** Peak PSS per process */
-        private Map<String, Long> mProcPss = new HashMap<>();
-        /** Peak USS per process */
-        private Map<String, Long> mProcUss = new HashMap<>();
-    }
-
-    // Memory usage data per device
-    private Map<ITestDevice, DeviceMemoryData> mMemoryData;
-    private Map<ITestDevice, Map<String, NumericValues.Builder>> mPssMemoryPerProcess;
-    private Map<ITestDevice, Map<String, NumericValues.Builder>> mUssMemoryPerProcess;
-
-    @Override
-    void onStart(DeviceMetricData runData) {
-        mMemoryData = new HashMap<>();
-        mPssMemoryPerProcess = new HashMap<>();
-        mUssMemoryPerProcess = new HashMap<>();
-
-        for (ITestDevice device : getDevices()) {
-            mMemoryData.put(device, new DeviceMemoryData());
-            mPssMemoryPerProcess.put(device, new HashMap<>());
-            mUssMemoryPerProcess.put(device, new HashMap<>());
-        }
-    }
-
-    @Override
-    void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        try {
-            Map<String, Long> procPss = mMemoryData.get(device).mProcPss;
-            Map<String, Long> procUss = mMemoryData.get(device).mProcUss;
-            for (String proc : mProcessNames) {
-                String dumpResult = device.executeShellCommand("dumpsys meminfo --checkin " + proc);
-                if (dumpResult.startsWith("No process found")) {
-                    // process not found, skip
-                    continue;
-                }
-                DumpsysProcessMeminfoItem item =
-                        new DumpsysProcessMeminfoParser()
-                                .parse(Arrays.asList(dumpResult.split("\n")));
-                Long pss =
-                        item.get(DumpsysProcessMeminfoItem.TOTAL)
-                                .get(DumpsysProcessMeminfoItem.PSS);
-                Long uss =
-                        item.get(DumpsysProcessMeminfoItem.TOTAL)
-                                .get(DumpsysProcessMeminfoItem.PRIVATE_DIRTY);
-                if (pss == null || uss == null) {
-                    CLog.e("Error parsing meminfo output: " + dumpResult);
-                    continue;
-                }
-
-                // Track PSS values
-                if (mPssMemoryPerProcess.get(device) == null) {
-                    mPssMemoryPerProcess.put(device, new HashMap<>());
-                }
-                if (mPssMemoryPerProcess.get(device).get(proc) == null) {
-                    mPssMemoryPerProcess.get(device).put(proc, NumericValues.newBuilder());
-                }
-                mPssMemoryPerProcess.get(device).get(proc).addNumericValue(pss);
-
-                // Track USS values
-                if (mUssMemoryPerProcess.get(device) == null) {
-                    mUssMemoryPerProcess.put(device, new HashMap<>());
-                }
-                if (mUssMemoryPerProcess.get(device).get(proc) == null) {
-                    mUssMemoryPerProcess.get(device).put(proc, NumericValues.newBuilder());
-                }
-                mUssMemoryPerProcess.get(device).get(proc).addNumericValue(uss);
-
-                if (procPss.getOrDefault(proc, 0L) < pss) {
-                    procPss.put(proc, pss);
-                }
-                if (procUss.getOrDefault(proc, 0L) < uss) {
-                    procUss.put(proc, uss);
-                }
-            }
-        } catch (DeviceNotAvailableException e) {
-            CLog.e(e);
-        }
-    }
-
-    @Override
-    void onEnd(DeviceMetricData runData) {
-        for (ITestDevice device : getDevices()) {
-            // Report all the PSS data for each process
-            for (Entry<String, NumericValues.Builder> values :
-                    mPssMemoryPerProcess.get(device).entrySet()) {
-                Metric.Builder metric = Metric.newBuilder();
-                metric.setMeasurements(
-                                Measurements.newBuilder()
-                                        .setNumericValues(values.getValue().build()))
-                        .build();
-                metric.setUnit("kB").setType(DataType.RAW);
-                runData.addMetricForDevice(device, "PSS#" + values.getKey(), metric);
-            }
-
-            // Report all the USS data for each process
-            for (Entry<String, NumericValues.Builder> values :
-                    mUssMemoryPerProcess.get(device).entrySet()) {
-                Metric.Builder metric = Metric.newBuilder();
-                metric.setMeasurements(
-                                Measurements.newBuilder()
-                                        .setNumericValues(values.getValue().build()))
-                        .build();
-                metric.setUnit("kB").setType(DataType.RAW);
-                runData.addMetricForDevice(device, "USS#" + values.getKey(), metric);
-            }
-
-            // Continue reporting the max PSS / USS for compatibility
-            Map<String, Long> procPss = mMemoryData.get(device).mProcPss;
-            Map<String, Long> procUss = mMemoryData.get(device).mProcUss;
-            for (Entry<String, Long> pss : procPss.entrySet()) {
-                Metric.Builder metric = Metric.newBuilder();
-                metric.setMeasurements(
-                        Measurements.newBuilder().setSingleInt(pss.getValue()).build());
-                metric.setUnit("kB").setType(DataType.PROCESSED);
-                runData.addMetricForDevice(device, "MAX_PSS#" + pss.getKey(), metric);
-            }
-            for (Entry<String, Long> uss : procUss.entrySet()) {
-                Metric.Builder metric = Metric.newBuilder();
-                metric.setMeasurements(
-                        Measurements.newBuilder().setSingleInt(uss.getValue()).build());
-                metric.setUnit("kB").setType(DataType.PROCESSED);
-                runData.addMetricForDevice(device, "MAX_USS#" + uss.getKey(), metric);
-            }
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/ScheduleMultipleDeviceMetricCollector.java b/test_framework/com/android/tradefed/device/metric/ScheduleMultipleDeviceMetricCollector.java
deleted file mode 100644
index 347a4b9..0000000
--- a/test_framework/com/android/tradefed/device/metric/ScheduleMultipleDeviceMetricCollector.java
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device.metric;
-
-import com.android.tradefed.config.Option;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.ITestInvocationListener;
-
-import java.io.File;
-import java.lang.reflect.InvocationTargetException;
-import java.math.BigInteger;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Timer;
-import java.util.TimerTask;
-
-/**
- * A {@link IMetricCollector} that makes runs multiple metric collectors periodically. This is a
- * best effort scheduler. It makes the best effort to run the collectors at given intervals while
- * making sure that no two collectors are run at the same time.
- */
-public class ScheduleMultipleDeviceMetricCollector extends BaseDeviceMetricCollector {
-    @Option(
-        name = "metric-collection-intervals",
-        description = "The interval at which the collectors should run."
-    )
-    private Map<String, Long> mIntervalMs = new HashMap<>();
-
-    @Option(
-        name = "metric-storage-path",
-        description =
-                "Absolute path to a directory on host where the collected metrics will be stored."
-    )
-    private File mMetricStoragePath = new File(System.getProperty("java.io.tmpdir"));
-
-    @Option(
-        name = "metric-collector-command-classes",
-        description =
-                "Complete package name of a class which registers the commands to do the actual "
-                        + "job of collection. Can be repeated."
-    )
-    private List<String> mMetricCollectorClasses = new ArrayList<>();
-
-    // List of collectors to run.
-    private List<ScheduledDeviceMetricCollector> mMetricCollectors = new ArrayList<>();
-
-    // Time interval at which the commands should run.
-    private Map<ScheduledDeviceMetricCollector, Long> mMetricCollectorIntervals = new HashMap<>();
-
-    // Time when the commands to collect various metrics were last run.
-    private Map<ScheduledDeviceMetricCollector, Long> mLastUpdate = new HashMap<>();
-
-    private Timer mTimer;
-
-    private long mScheduleRate;
-
-    @Override
-    public ITestInvocationListener init(
-            IInvocationContext context, ITestInvocationListener listener) {
-        super.init(context, listener);
-        initMetricCollectors(context, listener);
-
-        return this;
-    }
-
-    /** Gets an instance of all the requested metric collectors. */
-    private void initMetricCollectors(
-            IInvocationContext context, ITestInvocationListener listener) {
-        for (String metricCollectorClass : mMetricCollectorClasses) {
-            try {
-                Class<?> klass = Class.forName(metricCollectorClass);
-
-                ScheduledDeviceMetricCollector singleMetricCollector =
-                        klass.asSubclass(ScheduledDeviceMetricCollector.class)
-                                .getDeclaredConstructor()
-                                .newInstance();
-
-                singleMetricCollector.init(context, listener);
-
-                mMetricCollectors.add(singleMetricCollector);
-            } catch (ClassNotFoundException
-                    | InstantiationException
-                    | IllegalAccessException
-                    | InvocationTargetException
-                    | NoSuchMethodException e) {
-                CLog.e("Class %s not found, skipping.", metricCollectorClass);
-                CLog.e(e);
-            }
-        }
-    }
-
-    @Override
-    public final void onTestRunStart(DeviceMetricData runData) {
-        if (mMetricCollectorClasses.isEmpty()) {
-            CLog.w("No single metric class provided. Skipping collection.");
-            return;
-        }
-
-        setupCollection();
-
-        if (mScheduleRate == 0) {
-            CLog.e(
-                    "Failed to get a valid interval for even one metric collector. "
-                            + "Please make sure that the collectors have non-zero intervals "
-                            + "specified as an argument to this class.");
-            return;
-        }
-
-        // TODO(b/70394486): Investigate if ScheduledThreadPool is better suited here so that we can
-        // schedule all the metrics in their own thread and create a common object which allows
-        // running of only one collector at a time.
-        mTimer = new Timer();
-
-        TimerTask timerTask =
-                new TimerTask() {
-                    @Override
-                    public void run() {
-                        collect(runData);
-                    }
-                };
-
-        mTimer.scheduleAtFixedRate(timerTask, 0, mScheduleRate);
-    }
-
-    /**
-     * Sets up the collection process by parsing all the args, retrieving the intervals from the
-     * args and initializing the last update value of each of the collectors to current time.
-     */
-    private void setupCollection() {
-        parseAllArgs();
-        for (ScheduledDeviceMetricCollector singleMetricCollector :
-                mMetricCollectorIntervals.keySet()) {
-            mLastUpdate.put(singleMetricCollector, System.currentTimeMillis());
-        }
-
-        mScheduleRate = gcdOfIntervals();
-    }
-
-    /**
-     * Runs all the requested collectors sequentially. Dumps the output in {@code
-     * mmResultsDirectory/outputDirFormat} of the collector prefixed.
-     *
-     * @param runData holds the filename of the metrics collected for each collector.
-     */
-    private void collect(DeviceMetricData runData) {
-        for (ScheduledDeviceMetricCollector singleMetricCollector :
-                mMetricCollectorIntervals.keySet()) {
-
-            Long elapsedTime = System.currentTimeMillis() - mLastUpdate.get(singleMetricCollector);
-
-            Long taskInterval = mMetricCollectorIntervals.get(singleMetricCollector);
-
-            if (elapsedTime >= taskInterval) {
-                try {
-                    for (ITestDevice device : getDevices()) {
-                        singleMetricCollector.collect(device, runData);
-                    }
-                    mLastUpdate.put(singleMetricCollector, System.currentTimeMillis());
-                } catch (InterruptedException e) {
-                    CLog.e("Exception during %s", singleMetricCollector.getClass());
-                    CLog.e(e);
-                }
-            }
-        }
-    }
-
-    /** Parse all the intervals provided in the command line. */
-    private void parseAllArgs() {
-        for (ScheduledDeviceMetricCollector metricCollector : mMetricCollectors) {
-            Long value = mIntervalMs.getOrDefault(metricCollector.getTag(), 0L);
-
-            if (value > 0) {
-                mMetricCollectorIntervals.put(metricCollector, value);
-            } else if (value < 0) {
-                throw new IllegalArgumentException(
-                        metricCollector.getClass() + " expects a non negative interval.");
-            }
-        }
-    }
-
-    /** Get the {@code scheduleRate} common to all tasks which is the gcd of all the intervals. */
-    private Long gcdOfIntervals() {
-        Collection<Long> intervals = mMetricCollectorIntervals.values();
-        if (intervals.isEmpty()) {
-            return 0L;
-        }
-        BigInteger gcdSoFar = new BigInteger(intervals.iterator().next().toString());
-
-        for (Long interval : intervals) {
-            gcdSoFar = gcdSoFar.gcd(new BigInteger(interval.toString()));
-        }
-
-        return gcdSoFar.longValue();
-    }
-
-    @Override
-    public final void onTestRunEnd(
-            DeviceMetricData runData, final Map<String, Metric> currentRunMetrics) {
-        if (mTimer != null) {
-            mTimer.cancel();
-            mTimer.purge();
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/ScheduledDeviceMetricCollector.java b/test_framework/com/android/tradefed/device/metric/ScheduledDeviceMetricCollector.java
deleted file mode 100644
index 409ac2b..0000000
--- a/test_framework/com/android/tradefed/device/metric/ScheduledDeviceMetricCollector.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import com.android.annotations.VisibleForTesting;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.util.FileUtil;
-
-import java.io.File;
-import java.io.IOException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Timer;
-import java.util.TimerTask;
-
-/**
- * A {@link IMetricCollector} that allows to run a collection task periodically at a set interval.
- */
-public abstract class ScheduledDeviceMetricCollector extends BaseDeviceMetricCollector {
-
-    @Option(
-        name = "fixed-schedule-rate",
-        description = "Schedule the timetask as a fixed schedule rate"
-    )
-    private boolean mFixedScheduleRate = false;
-
-    @Option(
-        name = "interval",
-        description = "the interval between two tasks being scheduled",
-        isTimeVal = true
-    )
-    private long mIntervalMs = 60 * 1000l;
-
-    private Timer timer;
-
-    @Override
-    public final void onTestRunStart(DeviceMetricData runData) {
-        CLog.d("starting with interval = %s", mIntervalMs);
-        onStart(runData);
-        timer = new Timer();
-        TimerTask timerTask =
-                new TimerTask() {
-                    @Override
-                    public void run() {
-                        try {
-                            for (ITestDevice device : getDevices()) {
-                                collect(device, runData);
-                            }
-                        } catch (InterruptedException e) {
-                            timer.cancel();
-                            Thread.currentThread().interrupt();
-                            CLog.e("Interrupted exception thrown from task:");
-                            CLog.e(e);
-                        }
-                    }
-                };
-
-        if (mFixedScheduleRate) {
-            timer.scheduleAtFixedRate(timerTask, 0, mIntervalMs);
-        } else {
-            timer.schedule(timerTask, 0, mIntervalMs);
-        }
-    }
-
-    @Override
-    public final void onTestRunEnd(
-            DeviceMetricData runData, final Map<String, Metric> currentRunMetrics) {
-        if (timer != null) {
-            timer.cancel();
-            timer.purge();
-        }
-        onEnd(runData);
-        CLog.d("finished");
-    }
-
-    /**
-     * Task periodically & asynchronously run during the test running on a specific device.
-     *
-     * @param device the {@link ITestDevice} the metric is associated to.
-     * @param runData the {@link DeviceMetricData} where to put metrics.
-     * @throws InterruptedException
-     */
-    abstract void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException;
-
-    /**
-     * Executed when entering this collector.
-     *
-     * @param runData the {@link DeviceMetricData} where to put metrics.
-     */
-    void onStart(DeviceMetricData runData) {
-        // Does nothing.
-    }
-
-    /**
-     * Executed when finishing this collector.
-     *
-     * @param runData the {@link DeviceMetricData} where to put metrics.
-     */
-    void onEnd(DeviceMetricData runData) {
-        // Does nothing.
-    }
-
-    /**
-     * Send all the output of a process from all the devices to a file.
-     *
-     * <p>Please note, metric collections should not overlap.
-     *
-     * @throws DeviceNotAvailableException
-     * @throws IOException
-     */
-    File saveProcessOutput(ITestDevice device, String command, String outputFileName)
-            throws DeviceNotAvailableException, IOException {
-        String output = device.executeShellCommand(command);
-
-        // Create the output file and dump the output of the command to this file.
-        File outputFile = new File(outputFileName);
-
-        FileUtil.writeToFile(output, outputFile);
-
-        return outputFile;
-    }
-
-    /**
-     * Create a suffix string to be appended at the end of each metric file to keep the name unique
-     * at each run.
-     *
-     * @return suffix string in the format year-month-date-hour-minute-seconds-milliseconds.
-     */
-    @VisibleForTesting
-    String getFileSuffix() {
-        return new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US).format(new Date());
-    }
-
-    /**
-     * Creates temporary directory to store the metric files.
-     *
-     * @return {@link File} directory with 'tmp' prefixed to its name to signify that its temporary.
-     * @throws IOException
-     */
-    @VisibleForTesting
-    File createTempDir() throws IOException {
-        return FileUtil.createTempDir(String.format("tmp_%s", getTag()));
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/TemperatureCollector.java b/test_framework/com/android/tradefed/device/metric/TemperatureCollector.java
deleted file mode 100644
index 2c38cd5..0000000
--- a/test_framework/com/android/tradefed/device/metric/TemperatureCollector.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static com.android.tradefed.targetprep.TemperatureThrottlingWaiter.DEVICE_TEMPERATURE_FILE_PATH_NAME;
-
-import com.android.tradefed.config.Option;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.metrics.proto.MetricMeasurement.DataType;
-import com.android.tradefed.metrics.proto.MetricMeasurement.DoubleValues;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * A {@link ScheduledDeviceMetricCollector} to measure min and max device temperature. Useful for
- * long duration performance tests to monitor if the device overheats.
- */
-public class TemperatureCollector extends ScheduledDeviceMetricCollector {
-
-    private static final String CELCIUS_UNIT = "celcius";
-
-    // Option name intentionally shared with TemperatureThrottlingWaiter
-    @Option(
-        name = DEVICE_TEMPERATURE_FILE_PATH_NAME,
-        description =
-                "Name of file that contains device temperature. "
-                        + "Example: /sys/class/hwmon/hwmon1/device/msm_therm"
-    )
-    private String mDeviceTemperatureFilePath = null;
-
-    @Option(
-        name = "device-temperature-file-regex",
-        description =
-                "Regex to parse temperature file. First group must be the temperature parsable"
-                        + "to Double. Default: Result:(\\d+) Raw:.*"
-    )
-    private String mDeviceTemperatureFileRegex = "Result:(\\d+) Raw:.*";
-
-    /**
-     * Stores the highest recorded temperature per device. Device will not be present in the map if
-     * no valid temperature was recorded.
-     */
-    private Map<ITestDevice, Double> mMaxDeviceTemps;
-
-    /**
-     * Stores the lowest recorded temperature per device. Device will not be present in the map if
-     * no valid temperature was recorded.
-     */
-    private Map<ITestDevice, Double> mMinDeviceTemps;
-
-    // Example: Result:32 Raw:7e51
-    private static Pattern mTemperatureRegex;
-
-    private Map<ITestDevice, DoubleValues.Builder> mValues;
-
-    @Override
-    void onStart(DeviceMetricData runData) {
-        mTemperatureRegex = Pattern.compile(mDeviceTemperatureFileRegex);
-        mMaxDeviceTemps = new HashMap<>();
-        mMinDeviceTemps = new HashMap<>();
-        mValues = new HashMap<>();
-    }
-
-    @Override
-    void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        if (mDeviceTemperatureFilePath == null) {
-            return;
-        }
-        try {
-            if (!device.isAdbRoot()) {
-                return;
-            }
-            Double temp = getTemperature(device);
-            if (temp == null) {
-                return;
-            }
-            if (mValues.get(device) == null) {
-                mValues.put(device, DoubleValues.newBuilder());
-            }
-            mValues.get(device).addDoubleValue(temp);
-            mMaxDeviceTemps.putIfAbsent(device, temp);
-            mMinDeviceTemps.putIfAbsent(device, temp);
-            if (mMaxDeviceTemps.get(device) < temp) {
-                mMaxDeviceTemps.put(device, temp);
-            }
-            if (mMinDeviceTemps.get(device) > temp) {
-                mMinDeviceTemps.put(device, temp);
-            }
-        } catch (DeviceNotAvailableException e) {
-            CLog.e(e);
-        }
-    }
-
-    private Double getTemperature(ITestDevice device) throws DeviceNotAvailableException {
-        String cmd = "cat " + mDeviceTemperatureFilePath;
-        String result = device.executeShellCommand(cmd).trim();
-        Matcher m = mTemperatureRegex.matcher(result);
-        if (m.matches()) {
-            return Double.parseDouble(m.group(1));
-        }
-        CLog.e("Error parsing temperature file output: " + result);
-        return null;
-    }
-
-    @Override
-    void onEnd(DeviceMetricData runData) {
-        for (ITestDevice device : getDevices()) {
-            DoubleValues.Builder values = mValues.get(device);
-            if (values != null) {
-                Metric.Builder metric = Metric.newBuilder();
-                metric.setMeasurements(
-                        Measurements.newBuilder().setDoubleValues(values.build()).build());
-                metric.setUnit(CELCIUS_UNIT).setType(DataType.RAW);
-                runData.addMetricForDevice(device, "temperature", metric);
-            }
-            // Report the max and min for compatibility
-            Double maxTemp = mMaxDeviceTemps.get(device);
-            if (maxTemp != null) {
-                Metric.Builder metric = Metric.newBuilder();
-                metric.setMeasurements(Measurements.newBuilder().setSingleDouble(maxTemp).build());
-                // Since we report some processed value report it as PROCESSED.
-                metric.setUnit(CELCIUS_UNIT).setType(DataType.PROCESSED);
-                runData.addMetricForDevice(device, "max_temperature", metric);
-            }
-            Double minTemp = mMinDeviceTemps.get(device);
-            if (minTemp != null) {
-                Metric.Builder metric = Metric.newBuilder();
-                metric.setMeasurements(Measurements.newBuilder().setSingleDouble(minTemp).build());
-                // Since we report some processed value report it as PROCESSED.
-                metric.setUnit(CELCIUS_UNIT).setType(DataType.PROCESSED);
-                runData.addMetricForDevice(device, "min_temperature", metric);
-            }
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/TraceMetricCollector.java b/test_framework/com/android/tradefed/device/metric/TraceMetricCollector.java
deleted file mode 100644
index f1cc72f..0000000
--- a/test_framework/com/android/tradefed/device/metric/TraceMetricCollector.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.google.common.io.Files;
-import java.io.File;
-import java.io.IOException;
-
-/** A {@link ScheduledDeviceMetricCollector} to collect kernel debug trace at regular intervals. */
-public class TraceMetricCollector extends ScheduledDeviceMetricCollector {
-    TraceMetricCollector() {
-        setTag("trace");
-    }
-
-    @Override
-    void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        try {
-            CLog.i("Running trace collector...");
-            String outputFileName = String.format("%s/trace-%s", createTempDir(), getFileSuffix());
-            File outputFile =
-                    saveProcessOutput(
-                            device, "cat /sys/kernel/debug/tracing/trace", outputFileName);
-            try (InputStreamSource source = new FileInputStreamSource(outputFile, true)) {
-                getInvocationListener()
-                        .testLog(
-                                Files.getNameWithoutExtension(outputFile.getName()),
-                                LogDataType.TEXT,
-                                source);
-            }
-        } catch (DeviceNotAvailableException | IOException e) {
-            CLog.e(e);
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java b/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java
index 618b6bc..cd80332 100644
--- a/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java
+++ b/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java
@@ -344,7 +344,14 @@
         for (Entry<FieldDescriptor, Object> entry : fields.entrySet()) {
             if (!(entry.getValue() instanceof Message) && !(entry.getValue() instanceof List)) {
                 if (isNumeric(entry.getValue().toString())) {
-                    // Construct the metric if it is numeric value.
+                    // Check if the current field has to be used as prefix for other fields
+                    // and add it to the list of prefixes.
+                    if (mPerfettoPrefixKeyFields.contains(entry.getKey().toString())) {
+                        keyPrefixOtherFields.add(String.format("%s-%s",
+                                entry.getKey().getName().toString(), entry.getValue().toString()));
+                        continue;
+                    }
+                    // Otherwise treat this numeric field as metric.
                     if (mNumberPattern.matcher(entry.getValue().toString()).matches()) {
                         convertedMetrics.put(
                                 entry.getKey().getName(),
@@ -355,9 +362,9 @@
                         convertedMetrics.put(
                                 entry.getKey().getName(),
                                 TfMetricProtoUtil.stringToMetric(
-                                                Long.toString(
-                                                        Double.valueOf(entry.getValue().toString())
-                                                                .longValue()))
+                                        Long.toString(
+                                                Double.valueOf(entry.getValue().toString())
+                                                        .longValue()))
                                         .toBuilder());
                     }
                 } else {
@@ -375,20 +382,6 @@
             }
         }
 
-        // Add prefix key to all the keys in current proto message which has numeric values.
-        Map<String, Metric.Builder> additionalConvertedMetrics =
-                new HashMap<String, Metric.Builder>();
-        for (String prefix : keyPrefixOtherFields) {
-            for (Map.Entry<String, Metric.Builder> currentMetric : convertedMetrics.entrySet()) {
-                additionalConvertedMetrics.put(String.format("%s-%s", prefix,
-                        currentMetric.getKey()), currentMetric.getValue());
-            }
-        }
-
-        // Not cleaning up the other metrics without prefix fields.
-        convertedMetrics.putAll(additionalConvertedMetrics);
-
-
         // Recursively expand the proto messages and repeated fields(i.e list).
         // Recursion when there are no messages or list with in the current message.
         for (Entry<FieldDescriptor, Object> entry : fields.entrySet()) {
@@ -458,6 +451,20 @@
                 }
             }
         }
+
+        // Add prefix key to all the keys in current proto message which has numeric values.
+        Map<String, Metric.Builder> additionalConvertedMetrics =
+                new HashMap<String, Metric.Builder>();
+        for (String prefix : keyPrefixOtherFields) {
+            for (Map.Entry<String, Metric.Builder> currentMetric : convertedMetrics.entrySet()) {
+                additionalConvertedMetrics.put(String.format("%s-%s", prefix,
+                        currentMetric.getKey()), currentMetric.getValue());
+            }
+        }
+
+        // Not cleaning up the other metrics without prefix fields.
+        convertedMetrics.putAll(additionalConvertedMetrics);
+
         return convertedMetrics;
     }
 
@@ -511,3 +518,4 @@
         return mProcessedMetric ? DataType.PROCESSED : DataType.RAW;
     }
 }
+
diff --git a/test_framework/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java b/test_framework/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java
index 977de89..7836678 100644
--- a/test_framework/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java
+++ b/test_framework/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java
@@ -89,7 +89,7 @@
             throw new TargetSetupError(
                     "Failed to find a valid test zip directory.",
                     device.getDeviceDescriptor(),
-                    InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+                    InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
         }
         if (mForceQueryable && device.isAppEnumerationSupported()) {
             mInstallArgs.add("--force-queryable");
@@ -112,7 +112,7 @@
             throw new TargetSetupError(
                     "Invalid test zip directory!",
                     device.getDeviceDescriptor(),
-                    InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+                    InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
         }
         CLog.d("Installing all apks found in dir %s ...", directory.getAbsolutePath());
         File[] files = directory.listFiles();
diff --git a/test_framework/com/android/tradefed/targetprep/ArtChrootPreparer.java b/test_framework/com/android/tradefed/targetprep/ArtChrootPreparer.java
new file mode 100644
index 0000000..0d32890
--- /dev/null
+++ b/test_framework/com/android/tradefed/targetprep/ArtChrootPreparer.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.targetprep;
+
+import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.command.remote.DeviceDescriptor;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipFile;
+
+/** Create chroot directory for ART tests. */
+@OptionClass(alias = "art-chroot-preparer")
+public class ArtChrootPreparer extends BaseTargetPreparer {
+
+    // Predefined location of the chroot root directory.
+    public static final String CHROOT_PATH = "/data/local/tmp/art-test-chroot";
+
+    // Directories to create in the chroot.
+    private static final String[] MKDIRS = {
+        "/", "/apex", "/data", "/data/dalvik-cache", "/data/local/tmp", "/tmp",
+    };
+
+    // System mount points to replicate in the chroot.
+    private static final String[] MOUNTS = {
+        "/dev", "/linkerconfig", "/proc", "/sys", "/system", "/apex/com.android.os.statsd",
+    };
+
+    @Override
+    public void setUp(TestInformation testInfo)
+            throws TargetSetupError, BuildError, DeviceNotAvailableException {
+        ITestDevice device = testInfo.getDevice();
+
+        // Ensure there are no files left from previous runs.
+        cleanup(device);
+
+        // Create directories required for ART testing in chroot.
+        for (String dir : MKDIRS) {
+            adbShell(device, "mkdir -p " + CHROOT_PATH + dir);
+        }
+
+        // Replicate system mount point in the chroot.
+        for (String dir : MOUNTS) {
+            adbShell(device, "mkdir -p " + CHROOT_PATH + dir);
+            adbShell(device, "mount --bind " + dir + " " + CHROOT_PATH + dir);
+        }
+
+        // Activate APEXes in the chroot.
+        IBuildInfo buildInfo = testInfo.getBuildInfo();
+        IDeviceBuildInfo deviceBuild = (IDeviceBuildInfo) buildInfo;
+        DeviceDescriptor deviceDesc = device.getDeviceDescriptor();
+        File tests_dir = deviceBuild.getFile(BuildInfoFileKey.TARGET_LINKED_DIR);
+        // The art_chroot is a shared module containing comment ART test data.
+        File apexes_dir = FileUtil.getFileForPath(tests_dir, "art_chroot", "system", "apex");
+        if (apexes_dir.listFiles() == null) {
+            throw new TargetSetupError(
+                    "No apex files found in " + apexes_dir.getPath(), deviceDesc);
+        }
+        File tempDir = null;
+        try {
+            tempDir = FileUtil.createTempDir("art-test-apex");
+            for (File apex : apexes_dir.listFiles()) {
+                activateApex(device, tempDir, apex);
+            }
+        } catch (IOException e) {
+            throw new TargetSetupError("Error when activating apex", e, deviceDesc);
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    private void activateApex(ITestDevice device, File tempDir, File apex)
+            throws TargetSetupError, IOException, DeviceNotAvailableException {
+        CLog.i("Activate apex in ART chroot: " + apex.getName());
+        ZipFile apex_zip = new ZipFile(apex);
+        ZipArchiveEntry apex_payload = apex_zip.getEntry("apex_payload.img");
+        File temp = FileUtil.createTempFile("payload-", ".img", tempDir);
+        FileUtil.writeToFile(apex_zip.getInputStream(apex_payload), temp);
+        String deviceApexDir = CHROOT_PATH + "/apex/" + apex.getName();
+        // Rename "com.android.art.testing.apex" to just "com.android.art.apex".
+        deviceApexDir = deviceApexDir.replace(".testing.apex", "").replace(".apex", "");
+        String deviceApexImg = deviceApexDir + ".img";
+        if (!device.pushFile(temp, deviceApexImg)) {
+            throw new TargetSetupError(
+                    "adb push failed for " + apex.getName(), device.getDeviceDescriptor());
+        }
+        // TODO(b/168048638): Work-around for cuttlefish: first losetup call always fails.
+        device.executeShellV2Command("losetup -f");
+        // Mount the apex file via a loopback device.
+        String loopbackDevice = adbShell(device, "losetup -f -s " + deviceApexImg);
+        adbShell(device, "mkdir -p " + deviceApexDir);
+        adbShell(device, "mount -o loop,ro " + loopbackDevice + " " + deviceApexDir);
+    }
+
+    @Override
+    public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
+        try {
+            cleanup(testInfo.getDevice());
+        } catch (TargetSetupError ex) {
+            CLog.e("Tear-down failed: " + ex.toString());
+        }
+    }
+
+    // Wrapper for executeShellV2Command that checks that the command succeeds.
+    private String adbShell(ITestDevice device, String cmd)
+            throws TargetSetupError, DeviceNotAvailableException {
+        CommandResult result = device.executeShellV2Command(cmd);
+        if (result.getStatus() != CommandStatus.SUCCESS) {
+            throw new TargetSetupError(
+                    String.format(
+                            "adb shell command failed: '%s': %s".format(cmd, result.getStderr())));
+        }
+        return result.getStdout();
+    }
+
+    private void cleanup(ITestDevice device) throws TargetSetupError, DeviceNotAvailableException {
+        String mounts = adbShell(device, "mount");
+        Pattern pattern = Pattern.compile("^([^ ]+) on ([^ ]+) type ([^ ]+) .*$");
+        for (String mount : mounts.split("\n")) {
+            Matcher matcher = pattern.matcher(mount);
+            if (!matcher.matches()) {
+                throw new TargetSetupError("Failed to parse mount command output: " + mount);
+            }
+            if (matcher.group(2).startsWith(CHROOT_PATH)) {
+                adbShell(device, "umount " + matcher.group(2));
+            }
+        }
+        adbShell(device, "rm -rf " + CHROOT_PATH);
+    }
+}
diff --git a/test_framework/com/android/tradefed/targetprep/DynamicSystemPreparer.java b/test_framework/com/android/tradefed/targetprep/DynamicSystemPreparer.java
index ab335e7..d54cb49 100644
--- a/test_framework/com/android/tradefed/targetprep/DynamicSystemPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/DynamicSystemPreparer.java
@@ -26,11 +26,14 @@
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.SparseImageUtil;
 import com.android.tradefed.util.ZipUtil;
 import com.android.tradefed.util.ZipUtil2;
+
+import org.apache.commons.compress.archivers.zip.ZipFile;
+
 import java.io.File;
 import java.io.IOException;
-import org.apache.commons.compress.archivers.zip.ZipFile;
 
 /**
  * An {@link ITargetPreparer} that sets up a system image on top of a device build with the Dynamic
@@ -43,11 +46,15 @@
     private static final String DEST_PATH = "/sdcard/system.raw.gz";
 
     @Option(
-        name = "system-image-zip-name",
-        description = "The name of the zip file containing system.img."
-    )
+            name = "system-image-zip-name",
+            description = "The name of the zip file containing system.img.")
     private String mSystemImageZipName = "system-img.zip";
 
+    @Option(
+            name = "user-data-size-in-gb",
+            description = "Number of GB to be allocated for DSU user-data.")
+    private long mUserDataSizeInGb = 16L; // 16GB
+
     private boolean isDSURunning(ITestDevice device) throws DeviceNotAvailableException {
         CollectingOutputReceiver receiver = new CollectingOutputReceiver();
         device.executeShellCommand("gsi_tool status", receiver);
@@ -62,20 +69,26 @@
             throw new BuildError(
                     "Cannot find " + mSystemImageZipName + " in build info.",
                     device.getDeviceDescriptor(),
-                    InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+                    InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
         }
 
         ZipFile zipFile = null;
         File systemImage = null;
+        File rawSystemImage = null;
         File systemImageGZ = null;
         try {
             zipFile = new ZipFile(systemImageZipFile);
             systemImage = ZipUtil2.extractFileFromZip(zipFile, "system.img");
-            //     The prequest here is the system.img must be an unsparsed image.
-            //     Is there any way to detect the actual format and convert it accordingly.
+            if (SparseImageUtil.isSparse(systemImage)) {
+                rawSystemImage = FileUtil.createTempFile("system", ".raw");
+                SparseImageUtil.unsparse(systemImage, rawSystemImage);
+            } else {
+                // system.img is already non-sparse
+                rawSystemImage = systemImage;
+            }
             systemImageGZ = FileUtil.createTempFile("system", ".raw.gz");
-            long rawSize = systemImage.length();
-            ZipUtil.gzipFile(systemImage, systemImageGZ);
+            long rawSize = rawSystemImage.length();
+            ZipUtil.gzipFile(rawSystemImage, systemImageGZ);
             CLog.i("Pushing %s to %s", systemImageGZ.getAbsolutePath(), DEST_PATH);
             if (!device.pushFile(systemImageGZ, DEST_PATH)) {
                 throw new TargetSetupError(
@@ -95,17 +108,26 @@
                             + "--el KEY_SYSTEM_SIZE "
                             + rawSize
                             + " "
-                            + "--el KEY_USERDATA_SIZE 8589934592 "
-                            + "--ez KEY_ENABLE_WHEN_COMPLETED true";
+                            + "--el KEY_USERDATA_SIZE "
+                            + mUserDataSizeInGb * 1024 * 1024 * 1024
+                            + " --ez KEY_ENABLE_WHEN_COMPLETED true";
             device.executeShellCommand(command);
             // Check if device shows as unavailable (as expected after the activity finished).
-            device.waitForDeviceNotAvailable(DSU_MAX_WAIT_SEC * 1000);
-            device.waitForDeviceOnline();
-            // the waitForDeviceOnline may block and we need to correct the 'i'
-            // which is used to measure timeout accordingly
-            if (!isDSURunning(device)) {
+            if (!device.waitForDeviceNotAvailable(DSU_MAX_WAIT_SEC * 1000)) {
                 throw new TargetSetupError(
-                        "Timeout to boot into DSU", device.getDeviceDescriptor());
+                        "Timed out waiting for DSU installation to complete and reboot",
+                        device.getDeviceDescriptor());
+            }
+            try {
+                // waitForDeviceOnline() throws DeviceNotAvailableException if device does not
+                // become online within timeout.
+                device.waitForDeviceOnline();
+            } catch (DeviceNotAvailableException e) {
+                throw new TargetSetupError(
+                        "Timed out booting into DSU", e, device.getDeviceDescriptor());
+            }
+            if (!isDSURunning(device)) {
+                throw new TargetSetupError("Failed to boot into DSU", device.getDeviceDescriptor());
             }
             CommandResult result = device.executeShellV2Command("gsi_tool enable");
             if (CommandStatus.SUCCESS.equals(result.getStatus())) {
@@ -120,6 +142,7 @@
                     "fail to install the DynamicSystemUpdate", e, device.getDeviceDescriptor());
         } finally {
             FileUtil.deleteFile(systemImage);
+            FileUtil.deleteFile(rawSystemImage);
             FileUtil.deleteFile(systemImageGZ);
             ZipUtil2.closeZip(zipFile);
         }
diff --git a/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java b/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
index f3de9b4..0e61a42 100644
--- a/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
@@ -29,6 +29,9 @@
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
+import com.android.tradefed.result.error.ErrorIdentifier;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.IAbiReceiver;
 import com.android.tradefed.testtype.IInvocationContextReceiver;
@@ -137,9 +140,10 @@
      * Helper method to only throw if mAbortOnFailure is enabled. Callers should behave as if this
      * method may return.
      */
-    private void fail(String message, DeviceDescriptor descriptor) throws TargetSetupError {
+    private void fail(String message, DeviceDescriptor descriptor, ErrorIdentifier identifier)
+            throws TargetSetupError {
         if (shouldAbortOnFailure()) {
-            throw new TargetSetupError(message, descriptor);
+            throw new TargetSetupError(message, descriptor, identifier);
         } else {
             // Log the error and return
             Log.w(LOG_TAG, message);
@@ -153,7 +157,10 @@
         for (String pushspec : mPushSpecs) {
             String[] pair = pushspec.split("->");
             if (pair.length != 2) {
-                fail(String.format("Invalid pushspec: '%s'", Arrays.asList(pair)), descriptor);
+                fail(
+                        String.format("Invalid pushspec: '%s'", Arrays.asList(pair)),
+                        descriptor,
+                        InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR);
                 continue;
             }
             remoteToLocalMapping.put(pair[1], new File(pair[0]));
@@ -311,6 +318,22 @@
                 // approach to do individual download from remote artifact.
                 // Try to stage the files from remote zip files.
                 src = buildInfo.stageRemoteFile(fileName, testDir);
+                if (src != null) {
+                    try {
+                        // Search again with filtering on ABI
+                        File srcWithAbi = FileUtil.findFile(fileName, mAbi, testDir);
+                        if (srcWithAbi != null
+                                && !srcWithAbi
+                                        .getAbsolutePath()
+                                        .startsWith(src.getAbsolutePath())) {
+                            // When multiple matches are found, return the one with matching
+                            // ABI unless src is its parent directory.
+                            return srcWithAbi;
+                        }
+                    } catch (IOException e) {
+                        CLog.w("Failed to find test files with matching ABI from directory.");
+                    }
+                }
             }
         }
         return src;
@@ -388,7 +411,8 @@
         if (src == null || !src.exists()) {
             fail(
                     String.format("Local source file '%s' does not exist", localPath),
-                    device.getDeviceDescriptor());
+                    device.getDeviceDescriptor(),
+                    InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
             return;
         }
         if (src.isDirectory()) {
@@ -402,7 +426,8 @@
                         String.format(
                                 "Attempting to push dir '%s' to an existing device file '%s'",
                                 src.getAbsolutePath(), remotePath),
-                        device.getDeviceDescriptor());
+                        device.getDeviceDescriptor(),
+                        DeviceErrorIdentifier.FAIL_PUSH_FILE);
             }
             Set<String> filter = new HashSet<>();
             if (mAbi != null) {
@@ -415,7 +440,8 @@
                 fail(
                         String.format(
                                 "Failed to push local '%s' to remote '%s'", localPath, remotePath),
-                        device.getDeviceDescriptor());
+                        device.getDeviceDescriptor(),
+                        DeviceErrorIdentifier.FAIL_PUSH_FILE);
                 return;
             } else {
                 if (deleteContentOnly) {
@@ -428,7 +454,8 @@
                 fail(
                         String.format(
                                 "Failed to push local '%s' to remote '%s'", localPath, remotePath),
-                        device.getDeviceDescriptor());
+                        device.getDeviceDescriptor(),
+                        DeviceErrorIdentifier.FAIL_PUSH_FILE);
                 return;
             } else {
                 mFilesPushed.add(remotePath);
diff --git a/test_framework/com/android/tradefed/targetprep/PythonVirtualenvPreparer.java b/test_framework/com/android/tradefed/targetprep/PythonVirtualenvPreparer.java
index 7034330..90928da 100644
--- a/test_framework/com/android/tradefed/targetprep/PythonVirtualenvPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/PythonVirtualenvPreparer.java
@@ -26,6 +26,7 @@
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.PythonVirtualenvHelper;
 import com.android.tradefed.util.RunUtil;
 
 import java.io.File;
@@ -40,8 +41,7 @@
 @OptionClass(alias = "python-venv")
 public class PythonVirtualenvPreparer extends BaseTargetPreparer {
 
-    private static final String PIP = "pip";
-    private static final String PATH = "PATH";
+    private static final String PIP = "pip3";
     protected static final String PYTHONPATH = "PYTHONPATH";
     private static final int BASE_TIMEOUT = 1000 * 60;
 
@@ -70,6 +70,7 @@
 
     protected void installDeps(IBuildInfo buildInfo, ITestDevice device) throws TargetSetupError {
         boolean hasDependencies = false;
+        mPip = getPipPath();
         if (mRequirementsFile != null) {
             CommandResult c = mRunUtil.runTimedCmd(BASE_TIMEOUT * 5, mPip,
                     "install", "-r", mRequirementsFile.getAbsolutePath());
@@ -90,6 +91,9 @@
                     CLog.e("Installing %s failed", dep);
                     throw new TargetSetupError("Failed to install dependencies with pip",
                             device.getDeviceDescriptor());
+                } else {
+                    CLog.d("Successfullly installed %s.", dep);
+                    CLog.d("Stdout: %s", c.getStdout());
                 }
                 hasDependencies = true;
             }
@@ -99,9 +103,12 @@
         } else {
             // make the install directory of new packages available to other classes that
             // receive the build
-            buildInfo.setFile(PYTHONPATH, new File(mVenvDir,
-                    "local/lib/python2.7/site-packages"),
+            // TODO(b/166688272): Get install location from pip rather than hard code it.
+            buildInfo.setFile(
+                    PYTHONPATH,
+                    new File(mVenvDir, "local/lib/python3.8/site-packages"),
                     buildInfo.getBuildId());
+            buildInfo.setFile("VIRTUAL_ENV", mVenvDir, buildInfo.getBuildId());
         }
     }
 
@@ -109,13 +116,26 @@
             throws TargetSetupError {
         if (mVenvDir != null) {
             CLog.i("Using existing virtualenv based at %s", mVenvDir.getAbsolutePath());
-            activate();
+            PythonVirtualenvHelper.activate(mRunUtil, mVenvDir);
             return;
         }
+        checkVirtualenvVersion(device);
         try {
             mVenvDir = FileUtil.createNamedTempDir(buildInfo.getTestTag() + "-virtualenv");
-            mRunUtil.runTimedCmd(BASE_TIMEOUT, "virtualenv", mVenvDir.getAbsolutePath());
-            activate();
+            CommandResult c =
+                    mRunUtil.runTimedCmd(BASE_TIMEOUT, "virtualenv", mVenvDir.getAbsolutePath());
+            if (c.getStatus() != CommandStatus.SUCCESS) {
+                CLog.e("Creating virtual environment at %s failed.", mVenvDir.getAbsoluteFile());
+                CLog.e(
+                        "Status: %s\nStdout: %s\nStderr: %s",
+                        c.getStatus(), c.getStdout(), c.getStderr());
+                throw new TargetSetupError(
+                        String.format(
+                                "Failed to create virtual environment. Error:\n%s", c.getStderr()),
+                        device.getDeviceDescriptor());
+            }
+            CLog.i("Created a virtualenv based at %s", mVenvDir.getAbsolutePath());
+            PythonVirtualenvHelper.activate(mRunUtil, mVenvDir);
         } catch (IOException e) {
             CLog.e("Failed to create temp directory for virtualenv");
             throw new TargetSetupError("Error creating virtualenv", e,
@@ -131,13 +151,35 @@
         mRequirementsFile = f;
     }
 
-    private void activate() {
-        File binDir = new File(mVenvDir, "bin");
-        mRunUtil.setWorkingDir(binDir);
-        String path = System.getenv(PATH);
-        mRunUtil.setEnvVariable(PATH, binDir + ":" + path);
-        File pipFile = new File(binDir, PIP);
+    private String getPipPath() {
+        if (mVenvDir == null || !mVenvDir.exists()) {
+            return null;
+        }
+        String virtualenvPath = mVenvDir.getAbsolutePath();
+        File pipFile = new File(PythonVirtualenvHelper.getPythonBinDir(virtualenvPath), PIP);
         pipFile.setExecutable(true);
-        mPip = pipFile.getAbsolutePath();
+        return pipFile.getAbsolutePath();
+    }
+
+    /** Check if the virtualenv on the host is too old. */
+    private void checkVirtualenvVersion(ITestDevice device) throws TargetSetupError {
+        CommandResult result = mRunUtil.runTimedCmd(BASE_TIMEOUT, "virtualenv", "--version");
+        if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
+            throw new TargetSetupError(
+                    "Failed to run `virtualenv --version`. Reason:\n" + result.getStderr(),
+                    device.getDeviceDescriptor());
+        }
+        String stdout = result.getStdout(); // should start with 'virtualenv <version> from'
+        if (stdout.contains("command not found")) {
+            throw new TargetSetupError(
+                    "virtualenv is not installed.", device.getDeviceDescriptor());
+        }
+        String version = stdout.split(" ")[1];
+        int majorVersion = Integer.parseInt(version.split("\\.")[0]);
+        if (majorVersion < 20) {
+            throw new TargetSetupError(
+                    "virtualenv is too old. Required: >=20.0.1, yours: " + version,
+                    device.getDeviceDescriptor());
+        }
     }
 }
\ No newline at end of file
diff --git a/test_framework/com/android/tradefed/targetprep/RunHostCommandTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RunHostCommandTargetPreparer.java
index e3c485c..fd4c694 100644
--- a/test_framework/com/android/tradefed/targetprep/RunHostCommandTargetPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/RunHostCommandTargetPreparer.java
@@ -16,9 +16,11 @@
 
 package com.android.tradefed.targetprep;
 
+import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.IDeviceManager;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.ITestLogger;
@@ -86,6 +88,11 @@
     @Option(name = "host-cmd-timeout", description = "Timeout for each command specified.")
     private Duration mTimeout = Duration.ofMinutes(1L);
 
+    @Option(
+            name = "use-flashing-permit",
+            description = "Acquire a flashing permit before running commands.")
+    private boolean mUseFlashingPermit = false;
+
     private List<Process> mBgProcesses = new ArrayList<>();
     private List<BgCommandLog> mBgCommandLogs = new ArrayList<>();
     private ITestLogger mLogger;
@@ -147,7 +154,16 @@
         }
         ITestDevice device = testInfo.getDevice();
         replaceSerialNumber(mSetUpCommands, device);
-        runCommandList(mSetUpCommands, device);
+        try {
+            if (mUseFlashingPermit) {
+                getDeviceManager().takeFlashingPermit();
+            }
+            runCommandList(mSetUpCommands, device);
+        } finally {
+            if (mUseFlashingPermit) {
+                getDeviceManager().returnFlashingPermit();
+            }
+        }
 
         try {
             mBgCommandLogs = createBgCommandLogs();
@@ -164,9 +180,16 @@
         ITestDevice device = testInfo.getDevice();
         replaceSerialNumber(mTearDownCommands, device);
         try {
+            if (mUseFlashingPermit) {
+                getDeviceManager().takeFlashingPermit();
+            }
             runCommandList(mTearDownCommands, device);
         } catch (TargetSetupError tse) {
             CLog.e(tse);
+        } finally {
+            if (mUseFlashingPermit) {
+                getDeviceManager().returnFlashingPermit();
+            }
         }
 
         // Terminate background commands after test finished
@@ -273,6 +296,12 @@
         return mRunUtil;
     }
 
+    /** @return {@link IDeviceManager} instance used for flashing permits */
+    @VisibleForTesting
+    IDeviceManager getDeviceManager() {
+        return GlobalConfiguration.getDeviceManagerInstance();
+    }
+
     /**
      * Create a BgCommandLog object that is based on a temporary file for each background command
      *
diff --git a/test_framework/com/android/tradefed/targetprep/RunHostScriptTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RunHostScriptTargetPreparer.java
index def9252..6dd2b56 100644
--- a/test_framework/com/android/tradefed/targetprep/RunHostScriptTargetPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/RunHostScriptTargetPreparer.java
@@ -56,6 +56,11 @@
     @Option(name = "script-timeout", description = "Script execution timeout.")
     private Duration mTimeout = Duration.ofMinutes(1L);
 
+    @Option(
+            name = "use-flashing-permit",
+            description = "Acquire a flashing permit before executing the script.")
+    private boolean mUseFlashingPermit = false;
+
     private IRunUtil mRunUtil;
 
     @Override
@@ -82,28 +87,15 @@
         getRunUtil().setEnvVariable("ANDROID_SERIAL", device.getSerialNumber());
         setPathVariable(testInfo);
 
-        // Execute script and handle result
-        CommandResult result =
-                getRunUtil().runTimedCmd(mTimeout.toMillis(), scriptFile.getAbsolutePath());
-        switch (result.getStatus()) {
-            case SUCCESS:
-                CLog.i("Script executed successfully, stdout = [%s].", result.getStdout());
-                break;
-            case FAILED:
-                throw new TargetSetupError(
-                        String.format(
-                                "Script execution failed, stdout = [%s], stderr = [%s].",
-                                result.getStdout(), result.getStderr()),
-                        device.getDeviceDescriptor());
-            case TIMED_OUT:
-                throw new TargetSetupError(
-                        "Script execution timed out.", device.getDeviceDescriptor());
-            case EXCEPTION:
-                throw new TargetSetupError(
-                        String.format(
-                                "Exception during script execution, stdout = [%s], stderr = [%s].",
-                                result.getStdout(), result.getStderr()),
-                        device.getDeviceDescriptor());
+        try {
+            if (mUseFlashingPermit) {
+                getDeviceManager().takeFlashingPermit();
+            }
+            executeScript(scriptFile, device);
+        } finally {
+            if (mUseFlashingPermit) {
+                getDeviceManager().returnFlashingPermit();
+            }
         }
     }
 
@@ -116,7 +108,7 @@
         return mRunUtil;
     }
 
-    /** @return {@link IDeviceManager} instance used to fetch the configured adb/fastboot paths */
+    /** @return {@link IDeviceManager} instance used for adb/fastboot paths and flashing permits */
     @VisibleForTesting
     IDeviceManager getDeviceManager() {
         return GlobalConfiguration.getDeviceManagerInstance();
@@ -175,4 +167,35 @@
             getRunUtil().setEnvVariable("PATH", path);
         }
     }
+
+    /**
+     * Execute script and handle result.
+     *
+     * @param scriptFile script file to execute
+     * @param device device being prepared
+     */
+    private void executeScript(File scriptFile, ITestDevice device) throws TargetSetupError {
+        CommandResult result =
+                getRunUtil().runTimedCmd(mTimeout.toMillis(), scriptFile.getAbsolutePath());
+        switch (result.getStatus()) {
+            case SUCCESS:
+                CLog.i("Script executed successfully, stdout = [%s].", result.getStdout());
+                break;
+            case FAILED:
+                throw new TargetSetupError(
+                        String.format(
+                                "Script execution failed, stdout = [%s], stderr = [%s].",
+                                result.getStdout(), result.getStderr()),
+                        device.getDeviceDescriptor());
+            case TIMED_OUT:
+                throw new TargetSetupError(
+                        "Script execution timed out.", device.getDeviceDescriptor());
+            case EXCEPTION:
+                throw new TargetSetupError(
+                        String.format(
+                                "Exception during script execution, stdout = [%s], stderr = [%s].",
+                                result.getStdout(), result.getStderr()),
+                        device.getDeviceDescriptor());
+        }
+    }
 }
diff --git a/test_framework/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparer.java
new file mode 100644
index 0000000..7f2b28d
--- /dev/null
+++ b/test_framework/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparer.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.targetprep;
+
+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.UserInfo;
+import com.android.tradefed.invoker.TestInformation;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An {@link ITargetPreparer} that creates a secondary user in setup, and marks that tests should be
+ * run in that user.
+ *
+ * <p>In teardown, the secondary user is removed.
+ */
+@OptionClass(alias = "run-on-secondary-user")
+public class RunOnSecondaryUserTargetPreparer extends BaseTargetPreparer {
+
+    @VisibleForTesting static final String RUN_TESTS_AS_USER_KEY = "RUN_TESTS_AS_USER";
+
+    @VisibleForTesting static final String TEST_PACKAGE_NAME_OPTION = "test-package-name";
+
+    @Option(
+            name = TEST_PACKAGE_NAME_OPTION,
+            description =
+                    "the name of a package to be installed on the secondary user. "
+                            + "This must already be installed on the device.",
+            importance = Option.Importance.IF_UNSET)
+    private List<String> mTestPackages = new ArrayList<>();
+
+    @Override
+    public void setUp(TestInformation testInfo)
+            throws TargetSetupError, DeviceNotAvailableException {
+        int secondaryUserId = getSecondaryUserId(testInfo.getDevice());
+
+        if (secondaryUserId != -1) {
+            // There is already a secondary user - so we don't want to remove it
+            setDisableTearDown(true);
+        } else {
+            secondaryUserId = createSecondaryUser(testInfo.getDevice());
+        }
+
+        for (String pkg : mTestPackages) {
+            testInfo.getDevice()
+                    .executeShellCommand(
+                            "pm install-existing --user " + secondaryUserId + " " + pkg);
+        }
+
+        testInfo.properties().put(RUN_TESTS_AS_USER_KEY, Integer.toString(secondaryUserId));
+    }
+
+    /** Get the id of a secondary user currently on the device. -1 if there is none */
+    private static int getSecondaryUserId(ITestDevice device) throws DeviceNotAvailableException {
+        for (Map.Entry<Integer, UserInfo> userInfo : device.getUserInfos().entrySet()) {
+            if (userInfo.getValue().isSecondary()) {
+                return userInfo.getKey();
+            }
+        }
+        return -1;
+    }
+
+    /** Creates a secondary user and returns the new user ID. */
+    private static int createSecondaryUser(ITestDevice device) throws DeviceNotAvailableException {
+        final String createUserOutput = device.executeShellCommand("pm create-user secondary");
+        final int userId = Integer.parseInt(createUserOutput.split(" id ")[1].trim());
+        device.executeShellCommand("am start-user -w " + userId);
+        return userId;
+    }
+
+    @Override
+    public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
+        int userId = Integer.parseInt(testInfo.properties().get(RUN_TESTS_AS_USER_KEY));
+
+        testInfo.getDevice().removeUser(userId);
+    }
+}
diff --git a/test_framework/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparer.java
new file mode 100644
index 0000000..274a096
--- /dev/null
+++ b/test_framework/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparer.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.targetprep;
+
+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.UserInfo;
+import com.android.tradefed.invoker.TestInformation;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An {@link ITargetPreparer} that creates a work profile in setup, and marks that tests should be
+ * run in that user.
+ *
+ * <p>In teardown, the work profile is removed.
+ */
+@OptionClass(alias = "run-on-work-profile")
+public class RunOnWorkProfileTargetPreparer extends BaseTargetPreparer {
+
+    @VisibleForTesting static final String RUN_TESTS_AS_USER_KEY = "RUN_TESTS_AS_USER";
+
+    @VisibleForTesting static final String TEST_PACKAGE_NAME_OPTION = "test-package-name";
+
+    @Option(
+            name = TEST_PACKAGE_NAME_OPTION,
+            description =
+                    "the name of a package to be installed on the work profile. "
+                            + "This must already be installed on the device.",
+            importance = Option.Importance.IF_UNSET)
+    private List<String> mTestPackages = new ArrayList<>();
+
+    @Override
+    public void setUp(TestInformation testInfo)
+            throws TargetSetupError, DeviceNotAvailableException {
+        int workProfileId = getWorkProfileId(testInfo.getDevice());
+
+        if (workProfileId != -1) {
+            // There is already a work profile - so we don't want to remove it
+            setDisableTearDown(true);
+        } else {
+            workProfileId = createWorkProfile(testInfo.getDevice());
+        }
+
+        for (String pkg : mTestPackages) {
+            testInfo.getDevice()
+                    .executeShellCommand("pm install-existing --user " + workProfileId + " " + pkg);
+        }
+
+        testInfo.properties().put(RUN_TESTS_AS_USER_KEY, Integer.toString(workProfileId));
+    }
+
+    /** Get the id of a work profile currently on the device. -1 if there is none */
+    private static int getWorkProfileId(ITestDevice device) throws DeviceNotAvailableException {
+        for (Map.Entry<Integer, UserInfo> userInfo : device.getUserInfos().entrySet()) {
+            if (userInfo.getValue().isManagedProfile()) {
+                return userInfo.getKey();
+            }
+        }
+        return -1;
+    }
+
+    /** Creates a work profile and returns the new user ID. */
+    private static int createWorkProfile(ITestDevice device) throws DeviceNotAvailableException {
+        int parentProfile = device.getCurrentUser();
+        final String createUserOutput =
+                device.executeShellCommand(
+                        "pm create-user --profileOf " + parentProfile + " --managed work");
+        final int profileId = Integer.parseInt(createUserOutput.split(" id ")[1].trim());
+        device.executeShellCommand("am start-user -w " + profileId);
+        return profileId;
+    }
+
+    @Override
+    public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
+        int workProfileId = Integer.parseInt(testInfo.properties().get(RUN_TESTS_AS_USER_KEY));
+
+        testInfo.getDevice().removeUser(workProfileId);
+    }
+}
diff --git a/test_framework/com/android/tradefed/targetprep/WifiPreparer.java b/test_framework/com/android/tradefed/targetprep/WifiPreparer.java
index 1bfc635..b1f14e0 100644
--- a/test_framework/com/android/tradefed/targetprep/WifiPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/WifiPreparer.java
@@ -64,10 +64,14 @@
         if (mVerifyOnly) {
             if (!device.isWifiEnabled()) {
                 throw new TargetSetupError(
-                        "The device does not have wifi enabled.", device.getDeviceDescriptor());
+                        "The device does not have wifi enabled.",
+                        device.getDeviceDescriptor(),
+                        InfraErrorIdentifier.NO_WIFI);
             } else if (!device.checkConnectivity()) {
                 throw new TargetSetupError(
-                        "The device has no wifi connection.", device.getDeviceDescriptor());
+                        "The device has no wifi connection.",
+                        device.getDeviceDescriptor(),
+                        InfraErrorIdentifier.NO_WIFI);
             }
             return;
         }
diff --git a/test_framework/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java b/test_framework/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java
index c22324d..cec9d30 100644
--- a/test_framework/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java
@@ -96,15 +96,14 @@
     private Set<String> mSystemFileNames = new TreeSet<>();
 
     @Option(
-        name = "dummy-file-name",
-        description =
-                "the name of the image file to be replaced with a small dummy file. "
-                        + "Can be repeated. This option is used when the generic system "
-                        + "image is too large for the device's dynamic partition. "
-                        + "As GSI doesn't use product partition, the product image can be "
-                        + "replaced with a dummy file so as to free up space for GSI."
-    )
-    private Set<String> mDummyFileNames = new TreeSet<>();
+            name = "stub-file-name",
+            description =
+                    "the name of the image file to be replaced with a small stub file. "
+                            + "Can be repeated. This option is used when the generic system "
+                            + "image is too large for the device's dynamic partition. "
+                            + "As GSI doesn't use product partition, the product image can be "
+                            + "replaced with a stub file so as to free up space for GSI.")
+    private Set<String> mStubFileNames = new TreeSet<>();
 
     @Option(
         name = "compression-level",
@@ -218,21 +217,21 @@
             systemFiles = replaceExistingEntries(systemFiles, files);
             filesNotInDeviceBuild.putAll(systemFiles);
 
-            // Generate specified dummy files and replace those in device build.
-            Map<String, InputStreamFactory> dummyFiles =
-                    createDummyInputStreamFactories(mDummyFileNames);
-            Map<String, InputStreamFactory> dummyFilesNotInDeviceBuild =
-                    replaceExistingEntries(dummyFiles, files);
-            // The purpose of the dummy files is to make fastboot shrink product partition.
-            // Some devices don't have product partition and image. If the dummy file names are not
+            // Generate specified stub files and replace those in device build.
+            Map<String, InputStreamFactory> stubFiles =
+                    createStubInputStreamFactories(mStubFileNames);
+            Map<String, InputStreamFactory> stubFilesNotInDeviceBuild =
+                    replaceExistingEntries(stubFiles, files);
+            // The purpose of the stub files is to make fastboot shrink product partition.
+            // Some devices don't have product partition and image. If the stub file names are not
             // found in device build, they are ignored so that devices with and without product
             // partition can share configurations.
-            // This preparer does not generate dummy files in super image because
+            // This preparer does not generate stub files in super image because
             // build_super_image cannot handle unformatted files.
-            if (!dummyFilesNotInDeviceBuild.isEmpty()) {
+            if (!stubFilesNotInDeviceBuild.isEmpty()) {
                 CLog.w(
-                        "Skip creating dummy images: %s",
-                        String.join(",", dummyFilesNotInDeviceBuild.keySet()));
+                        "Skip creating stub images: %s",
+                        String.join(",", stubFilesNotInDeviceBuild.keySet()));
             }
 
             if (resourceBuildInfo != null) {
@@ -254,7 +253,7 @@
                     throw new BuildError(
                             "Cannot get " + MISC_INFO_FILE_NAME + " from device build.",
                             device.getDeviceDescriptor(),
-                            InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+                            InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
                 }
 
                 File otaToolsZip = mOtaToolsZip;
@@ -265,7 +264,7 @@
                     throw new BuildError(
                             "Cannot get " + OTATOOLS_ZIP_NAME + " from system build.",
                             systemNullDevice.getDeviceDescriptor(),
-                            InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+                            InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
                 }
 
                 File repackSuperImageFile = mRepackSuperImageFile;
@@ -276,7 +275,7 @@
                     throw new BuildError(
                             "Cannot get " + REPACK_SUPER_IMAGE_FILE_NAME + " from system build.",
                             systemNullDevice.getDeviceDescriptor(),
-                            InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+                            InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
                 }
 
                 mixedSuperImage = FileUtil.createTempFile("super", ".img");
@@ -376,14 +375,14 @@
         return factories;
     }
 
-    private static Map<String, InputStreamFactory> createDummyInputStreamFactories(
-            Collection<String> dummyFileNames) {
+    private static Map<String, InputStreamFactory> createStubInputStreamFactories(
+            Collection<String> stubFileNames) {
         // The image size must be larger than zero. Otherwise fastboot cannot flash it.
         byte[] data = new byte[] {0};
         Map<String, InputStreamFactory> factories = new HashMap<>();
-        for (String dummyFileName : dummyFileNames) {
+        for (String stubFileName : stubFileNames) {
             factories.put(
-                    dummyFileName,
+                    stubFileName,
                     new InputStreamFactory() {
                         @Override
                         public InputStream createInputStream() throws IOException {
@@ -598,8 +597,8 @@
     }
 
     @VisibleForTesting
-    void addDummyFileName(String fileName) {
-        mDummyFileNames.add(fileName);
+    void addStubFileName(String fileName) {
+        mStubFileNames.add(fileName);
     }
 
     @VisibleForTesting
diff --git a/test_framework/com/android/tradefed/testtype/ArtGTest.java b/test_framework/com/android/tradefed/testtype/ArtGTest.java
new file mode 100644
index 0000000..cf6a333
--- /dev/null
+++ b/test_framework/com/android/tradefed/testtype/ArtGTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.testtype;
+
+import com.android.tradefed.targetprep.ArtChrootPreparer;
+
+public class ArtGTest extends GTest {
+    @Override
+    protected String getGTestCmdLineWrapper(String fullPath, String flags) {
+        String chroot = ArtChrootPreparer.CHROOT_PATH;
+        if (fullPath.startsWith(chroot)) {
+            fullPath = fullPath.substring(chroot.length());
+        }
+        return String.format("chroot %s %s %s", chroot, fullPath, flags);
+    }
+}
diff --git a/test_framework/com/android/tradefed/testtype/ArtRunTest.java b/test_framework/com/android/tradefed/testtype/ArtRunTest.java
index a982104..e0dad70 100644
--- a/test_framework/com/android/tradefed/testtype/ArtRunTest.java
+++ b/test_framework/com/android/tradefed/testtype/ArtRunTest.java
@@ -17,35 +17,47 @@
 package com.android.tradefed.testtype;
 
 import com.android.ddmlib.CollectingOutputReceiver;
-
 import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
+import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.util.AbiUtils;
 import com.android.tradefed.util.ArrayUtil;
 import com.android.tradefed.util.FileUtil;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.HashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
 
 /** A test runner to run ART run-tests. */
-public class ArtRunTest implements IDeviceTest, IRemoteTest, IAbiReceiver {
+public class ArtRunTest implements IDeviceTest, IRemoteTest, IAbiReceiver, ITestFilterReceiver {
 
     private static final String RUNTEST_TAG = "ArtRunTest";
 
     private static final String DALVIKVM_CMD =
             "dalvikvm|#BITNESS#| -classpath |#CLASSPATH#| |#MAINCLASS#|";
+    public static final String CHECKER_EXECUTABLE = "art/tools/checker/checker.py";
 
     @Option(
             name = "test-timeout",
@@ -63,6 +75,8 @@
 
     private ITestDevice mDevice = null;
     private IAbi mAbi = null;
+    private Set<String> mIncludeFilters = new LinkedHashSet<>();
+    private Set<String> mExcludeFilters = new LinkedHashSet<>();
 
     /** {@inheritDoc} */
     @Override
@@ -89,6 +103,54 @@
 
     /** {@inheritDoc} */
     @Override
+    public void addIncludeFilter(String filter) {
+        mIncludeFilters.add(filter);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void addAllIncludeFilters(Set<String> filters) {
+        mIncludeFilters.addAll(filters);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void addExcludeFilter(String filter) {
+        mExcludeFilters.add(filter);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void addAllExcludeFilters(Set<String> filters) {
+        mExcludeFilters.addAll(filters);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Set<String> getIncludeFilters() {
+        return mIncludeFilters;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Set<String> getExcludeFilters() {
+        return mExcludeFilters;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void clearIncludeFilters() {
+        mIncludeFilters.clear();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void clearExcludeFilters() {
+        mExcludeFilters.clear();
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public void run(TestInformation testInfo, ITestInvocationListener listener)
             throws DeviceNotAvailableException {
         if (mDevice == null) {
@@ -111,24 +173,29 @@
      * Run a single ART run-test (on device).
      *
      * @param listener {@link ITestInvocationListener} listener for test
-     * @throws DeviceNotAvailableException
+     * @throws DeviceNotAvailableException If there was a problem communicating with
+     *      the test device.
      */
     void runArtTest(TestInformation testInfo, ITestInvocationListener listener)
             throws DeviceNotAvailableException {
+        String abi = mAbi.getName();
+        String runName = String.format("%s_%s", RUNTEST_TAG, abi);
+        TestDescription testId = new TestDescription(runName, mRunTestName);
+        if (shouldSkipCurrentTest(testId)) {
+            return;
+        }
+
         CLog.i("Running ArtRunTest %s on %s", mRunTestName, mDevice.getSerialNumber());
 
         String cmd = DALVIKVM_CMD;
-        String abi = mAbi.getName();
         cmd = cmd.replace("|#BITNESS#|", AbiUtils.getBitness(abi));
         cmd = cmd.replace("|#CLASSPATH#|", ArrayUtil.join(File.pathSeparator, mClasspath));
         // TODO: Turn this into an an option of the `ArtRunTest` class?
         cmd = cmd.replace("|#MAINCLASS#|", "Main");
 
         CLog.d("About to run run-test command: %s", cmd);
-        String runName = String.format("%s_%s", RUNTEST_TAG, abi);
         // Note: We only run one test at the moment.
         int testCount = 1;
-        TestDescription testId = new TestDescription(runName, mRunTestName);
         listener.testRunStarted(runName, testCount);
         listener.testStarted(testId);
 
@@ -143,7 +210,9 @@
             // Check the output producted by the test.
             if (output != null) {
                 try {
-                    File expectedFile = getDependencyFileFromRunTestDir(testInfo, "expected.txt");
+                    String expectedFileName = String.format("%s-expected.txt", mRunTestName);
+                    File expectedFile =
+                            testInfo.getDependencyFile(expectedFileName, /* targetFirst */ true);
                     CLog.i("Found expected output for run-test %s: %s", mRunTestName, expectedFile);
                     String expected = FileUtil.readStringFromFile(expectedFile);
                     if (!output.equals(expected)) {
@@ -153,47 +222,145 @@
                         // current ART run-test scripts).
                         CLog.i("%s FAILED: %s", mRunTestName, error);
                         listener.testFailed(testId, error);
+                        return;
                     }
                 } catch (IOException ioe) {
                     CLog.e(
                             "I/O error while accessing expected output file for test %s: %s",
                             mRunTestName, ioe);
                     listener.testFailed(testId, "I/O error while accessing expected output file.");
+                    return;
                 }
             } else {
                 listener.testFailed(testId, "No output received to compare to.");
+                return;
+            }
+
+            if (mRunTestName.contains("-checker-")) {
+                // not particularly reliable way of constructing a temporary dir
+                String cfgPathDir =
+                        String.format("/data/local/tmp/%s", mRunTestName.replaceAll("/", "-"));
+                mDevice.executeShellCommand(String.format("mkdir -p \"%s\"", cfgPathDir));
+
+                String cfgPath = cfgPathDir + "/graph.cfg";
+                mDevice.executeShellCommand(
+                        String.format(
+                                "dex2oat --dex-file=%s --oat-file=/dev/null --dump-cfg=%s -j1",
+                                mClasspath.get(0), cfgPath));
+
+                File runTestDir;
+                try {
+                    runTestDir = getRunTestDir(testInfo);
+                } catch (FileNotFoundException e) {
+                    listener.testFailed(testId, "I/O error while accessing test dir.");
+                    return;
+                }
+
+                File localCfgPath = new File(runTestDir, "graph.cfg");
+                if (localCfgPath.isFile()) {
+                    localCfgPath.delete();
+                }
+
+                mDevice.pullFile(cfgPath, localCfgPath);
+
+                File tempJar = new File(runTestDir, "temp.jar");
+                mDevice.pullFile(mClasspath.get(0), tempJar);
+
+                try (ZipFile archive = new ZipFile(tempJar)) {
+                    File srcFile = new File(runTestDir, "src");
+                    if (srcFile.exists()) {
+                        Files.walk(srcFile.toPath())
+                                .map(Path::toFile)
+                                .sorted(Comparator.reverseOrder())
+                                .forEach(File::delete);
+                    }
+
+                    List<? extends ZipEntry> entries = archive.stream()
+                            .sorted(Comparator.comparing(ZipEntry::getName))
+                            .collect(Collectors.toList());
+
+                    for (ZipEntry entry : entries) {
+                        if (entry.getName().startsWith("src")) {
+                            Path entryDest = runTestDir.toPath().resolve(entry.getName());
+                            if (entry.isDirectory()) {
+                                Files.createDirectory(entryDest);
+                            } else {
+                                Files.copy(archive.getInputStream(entry), entryDest);
+                            }
+                        }
+                    }
+                } catch (IOException e) {
+                    listener.testFailed(testId, "Error unpacking test jar");
+                    CLog.e("Jar unpacking failed with exception %s", e);
+                    CLog.e(e);
+                    return;
+                }
+
+                String checkerArch = AbiUtils.getArchForAbi(abi).toUpperCase();
+
+                ProcessBuilder processBuilder =
+                        new ProcessBuilder(
+                                CHECKER_EXECUTABLE,
+                                "-q",
+                                "--arch=" + checkerArch,
+                                localCfgPath.getAbsolutePath(),
+                                runTestDir.getAbsolutePath());
+
+                try {
+                    Process process = processBuilder.start();
+                    if (process.waitFor() != 0) {
+                        String checkerOutput = new BufferedReader(
+                                new InputStreamReader(process.getErrorStream())).lines().collect(
+                                Collectors.joining("\n"));
+                        listener.testFailed(testId, "Checker failed\n" + checkerOutput);
+                        listener.testLog("graph.cfg", LogDataType.CFG,
+                                new FileInputStreamSource(localCfgPath));
+                    }
+                } catch (IOException | InterruptedException e) {
+                    listener.testFailed(testId, "I/O error while starting Checker process");
+                }
             }
         } finally {
-            HashMap<String, Metric> emptyTestMetrics = new HashMap();
+            HashMap<String, Metric> emptyTestMetrics = new HashMap<>();
             listener.testEnded(testId, emptyTestMetrics);
-            HashMap<String, Metric> emptyTestRunMetrics = new HashMap();
+            HashMap<String, Metric> emptyTestRunMetrics = new HashMap<>();
             // TODO: Pass an actual value as `elapsedTimeMillis` argument.
             listener.testRunEnded(/* elapsedTimeMillis*/ 0, emptyTestRunMetrics);
         }
     }
 
+    /**
+     * Check if current test should be skipped.
+     *
+     * @param description The test in progress.
+     * @return true if the test should be skipped.
+     */
+    private boolean shouldSkipCurrentTest(TestDescription description) {
+        // Force to skip any test not listed in include filters, or listed in exclude filters.
+        // exclude filters have highest priority.
+        String testName = description.getTestName();
+        String descString = description.toString();
+        if (mExcludeFilters.contains(testName) || mExcludeFilters.contains(descString)) {
+            return true;
+        }
+        if (!mIncludeFilters.isEmpty()) {
+            return !mIncludeFilters.contains(testName) && !mIncludeFilters.contains(descString);
+        }
+        return false;
+    }
+
     /** Create an output receiver for the test command executed on the device. */
     protected CollectingOutputReceiver createTestOutputReceiver() {
         return new CollectingOutputReceiver();
     }
 
-    /** Search for a dependency/artifact file in the run-test's directory. */
-    protected File getDependencyFileFromRunTestDir(TestInformation testInfo, String fileName)
-            throws FileNotFoundException {
+    private File getRunTestDir(TestInformation testInfo) throws FileNotFoundException {
         File testsDir = testInfo.executionFiles().get(FilesKey.TARGET_TESTS_DIRECTORY);
         if (testsDir == null || !testsDir.exists()) {
             throw new FileNotFoundException(
                     String.format(
                             "Could not find target tests directory for test %s.", mRunTestName));
         }
-        File runTestDir = new File(testsDir, mRunTestName);
-        File file = FileUtil.findFile(runTestDir, fileName);
-        if (file == null) {
-            throw new FileNotFoundException(
-                    String.format(
-                            "Could not find an artifact file associated with %s in directory %s.",
-                            fileName, runTestDir));
-        }
-        return file;
+        return new File(testsDir, mRunTestName);
     }
 }
diff --git a/test_framework/com/android/tradefed/testtype/GTest.java b/test_framework/com/android/tradefed/testtype/GTest.java
index 7735e33..eb920e9 100644
--- a/test_framework/com/android/tradefed/testtype/GTest.java
+++ b/test_framework/com/android/tradefed/testtype/GTest.java
@@ -16,11 +16,6 @@
 
 package com.android.tradefed.testtype;
 
-import static com.android.tradefed.testtype.coverage.CoverageOptions.Toolchain.CLANG;
-import static com.android.tradefed.testtype.coverage.CoverageOptions.Toolchain.GCOV;
-
-import static com.google.common.base.Verify.verify;
-
 import com.android.ddmlib.FileListingService;
 import com.android.ddmlib.IShellOutputReceiver;
 import com.android.tradefed.config.Option;
@@ -31,10 +26,8 @@
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.testtype.coverage.CoverageOptions;
 import com.android.tradefed.util.AbiUtils;
 import com.android.tradefed.util.FileUtil;
-import com.android.tradefed.util.NativeCodeCoverageFlusher;
 
 import com.google.common.annotations.VisibleForTesting;
 
@@ -156,6 +149,10 @@
         return testPath.toString();
     }
 
+    public void setNativeTestDevicePath(String path) {
+        mNativeTestDevicePath = path;
+    }
+
     /**
      * Executes all native tests in a folder as well as in all subfolders recursively.
      *
@@ -427,26 +424,10 @@
         if (mStopRuntime) {
             mDevice.executeShellCommand("stop");
         }
-        // Insert the coverage listener if code coverage collection is enabled.
-        listener = addNativeCoverageListenerIfEnabled(listener);
-        listener = addClangCoverageListenerIfEnabled(listener);
         listener = getGTestListener(listener);
-        NativeCodeCoverageFlusher flusher =
-                new NativeCodeCoverageFlusher(mDevice, getCoverageOptions().getCoverageProcesses());
 
         Throwable throwable = null;
         try {
-            if (getCoverageOptions().isCoverageEnabled()) {
-                // Enable abd root on the device, otherwise the following commands will fail.
-                verify(mDevice.enableAdbRoot(), "Failed to enable adb root.");
-
-                flusher.resetCoverage();
-
-                // Clang will no longer create directories that are part of the GCOV_PREFIX
-                // environment variable. Force create the /data/misc/trace/testcoverage dir to
-                // prevent "No such file or directory" errors when writing test coverage to disk.
-                mDevice.executeShellCommand("mkdir /data/misc/trace/testcoverage");
-            }
             doRunAllTestsInSubdirectory(testPath, mDevice, listener);
         } catch (Throwable t) {
             throwable = t;
@@ -460,41 +441,4 @@
             }
         }
     }
-
-    /**
-     * Adds a listener to pull native code coverage measurements from the device after the test is
-     * complete if coverage is enabled, otherwise returns the same listener.
-     *
-     * @param listener the current chain of listeners
-     * @return a native coverage listener if coverage is enabled, otherwise the original listener
-     */
-    private ITestInvocationListener addNativeCoverageListenerIfEnabled(
-            ITestInvocationListener listener) {
-        CoverageOptions options = getCoverageOptions();
-
-        if (options.isCoverageEnabled() && options.getCoverageToolchains().contains(GCOV)) {
-            return new NativeCodeCoverageListener(mDevice, options, listener);
-        }
-        return listener;
-    }
-
-    /**
-     * Adds a listener to pull Clang code coverage measurements from the device after the test is
-     * complete if coverage is enabled, otherwise returns the same listener.
-     *
-     * @param listener the current chain of listeners
-     * @return a native coverage listener if coverage is enabled, otherwise the original listener
-     */
-    private ITestInvocationListener addClangCoverageListenerIfEnabled(
-            ITestInvocationListener listener) {
-        CoverageOptions options = getCoverageOptions();
-
-        if (options.isCoverageEnabled() && options.getCoverageToolchains().contains(CLANG)) {
-            ClangCodeCoverageListener clangListener =
-                    new ClangCodeCoverageListener(mDevice, listener);
-            clangListener.setConfiguration(getConfiguration());
-            return clangListener;
-        }
-        return listener;
-    }
 }
diff --git a/test_framework/com/android/tradefed/testtype/GTestBase.java b/test_framework/com/android/tradefed/testtype/GTestBase.java
index 3ee7179..5b22e66 100644
--- a/test_framework/com/android/tradefed/testtype/GTestBase.java
+++ b/test_framework/com/android/tradefed/testtype/GTestBase.java
@@ -17,6 +17,7 @@
 package com.android.tradefed.testtype;
 
 import com.android.ddmlib.IShellOutputReceiver;
+import com.android.tradefed.config.Configuration;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.IConfigurationReceiver;
 import com.android.tradefed.config.Option;
@@ -24,7 +25,6 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.testtype.coverage.CoverageOptions;
 import com.android.tradefed.util.ArrayUtil;
 import com.android.tradefed.util.FileUtil;
 
@@ -547,6 +547,14 @@
     }
 
     /**
+     * Helper which allows derived classes to wrap the gtest command under some other tool (chroot,
+     * strace, gdb, and similar).
+     */
+    protected String getGTestCmdLineWrapper(String fullPath, String flags) {
+        return String.format("%s %s", fullPath, flags);
+    }
+
+    /**
      * Helper method to build the gtest command to run.
      *
      * @param fullPath absolute file system path to gtest binary on device
@@ -559,16 +567,12 @@
             gTestCmdLine.append(String.format("LD_LIBRARY_PATH=%s ", mLdLibraryPath));
         }
 
-        if (getCoverageOptions().isCoverageEnabled()) {
-            gTestCmdLine.append("GCOV_PREFIX=/data/misc/trace/testcoverage ");
-        }
-
         // su to requested user
         if (mRunTestAs != null) {
             gTestCmdLine.append(String.format("su %s ", mRunTestAs));
         }
 
-        gTestCmdLine.append(String.format("%s %s", fullPath, flags));
+        gTestCmdLine.append(getGTestCmdLineWrapper(fullPath, flags));
         return gTestCmdLine.toString();
     }
 
@@ -665,18 +669,10 @@
      * @return an IConfiguration
      */
     protected IConfiguration getConfiguration() {
-        return mConfiguration;
-    }
-
-    /**
-     * Returns the {@link CoverageOptions} for this test, if it exists. Otherwise returns a default
-     * {@link CoverageOptions} object with all coverage disabled.
-     */
-    protected CoverageOptions getCoverageOptions() {
-        if (mConfiguration != null) {
-            return mConfiguration.getCoverageOptions();
+        if (mConfiguration == null) {
+            return new Configuration("", "");
         }
-        return new CoverageOptions();
+        return mConfiguration;
     }
 
     /**
diff --git a/test_framework/com/android/tradefed/testtype/GoogleBenchmarkTest.java b/test_framework/com/android/tradefed/testtype/GoogleBenchmarkTest.java
index 373844a..837ba81 100644
--- a/test_framework/com/android/tradefed/testtype/GoogleBenchmarkTest.java
+++ b/test_framework/com/android/tradefed/testtype/GoogleBenchmarkTest.java
@@ -26,6 +26,7 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
+import com.android.tradefed.util.StringEscapeUtils;
 
 import com.google.common.annotations.VisibleForTesting;
 
@@ -406,7 +407,7 @@
         if (iterator.hasNext()) {
             filterFlag.append(String.format(" %s=%s", GBENCHMARK_FILTER_OPTION, iterator.next()));
             while (iterator.hasNext()) {
-                filterFlag.append(String.format("\\|%s", iterator.next()));
+                filterFlag.append(String.format("|%s", iterator.next()));
             }
         }
         return filterFlag.toString();
@@ -421,7 +422,7 @@
             // Format benchmark as "^benchmark$" to avoid unintended regex partial matching.
             filterFlag.append(String.format(" %s=^%s$", GBENCHMARK_FILTER_OPTION, iterator.next()));
             while (iterator.hasNext()) {
-                filterFlag.append(String.format("\\|^%s$", iterator.next()));
+                filterFlag.append(String.format("|^%s$", iterator.next()));
             }
         }
         return filterFlag.toString();
@@ -441,14 +442,14 @@
             final String cmd,
             final IShellOutputReceiver outputReceiver)
             throws DeviceNotAvailableException {
+        String shellCmd = StringEscapeUtils.escapeShell(cmd);
         // Ensure that command is not too long for adb
-        if (cmd.length() < ADB_CMD_CHAR_LIMIT) {
+        if (shellCmd.length() < ADB_CMD_CHAR_LIMIT) {
             if (outputReceiver == null) {
-                return testDevice.executeShellCommand(cmd);
+                return testDevice.executeShellCommand(shellCmd);
             }
-
             testDevice.executeShellCommand(
-                    cmd,
+                    shellCmd,
                     outputReceiver,
                     mMaxRunTime /* maxTimeToShellOutputResponse */,
                     TimeUnit.MILLISECONDS,
@@ -457,7 +458,7 @@
         }
 
         // Wrap adb shell command in script if command is too long for direct execution
-        return executeCommandByScript(testDevice, cmd, outputReceiver);
+        return executeCommandByScript(testDevice, shellCmd, outputReceiver);
     }
 
     /** Runs a command from a temporary script. */
diff --git a/test_framework/com/android/tradefed/testtype/HostGTest.java b/test_framework/com/android/tradefed/testtype/HostGTest.java
index 43d599e..3da63e4 100644
--- a/test_framework/com/android/tradefed/testtype/HostGTest.java
+++ b/test_framework/com/android/tradefed/testtype/HostGTest.java
@@ -41,21 +41,10 @@
 
 /** A Test that runs a native test package. */
 @OptionClass(alias = "hostgtest")
-public class HostGTest extends GTestBase implements IAbiReceiver, IBuildReceiver {
+public class HostGTest extends GTestBase implements IBuildReceiver {
     private static final long DEFAULT_HOST_COMMAND_TIMEOUT_MS = 2 * 60 * 1000;
 
     private IBuildInfo mBuildInfo = null;
-    private IAbi mAbi = null;
-
-    @Override
-    public void setAbi(IAbi abi) {
-        this.mAbi = abi;
-    }
-
-    @Override
-    public IAbi getAbi() {
-        return this.mAbi;
-    }
 
     @Override
     public void setBuild(IBuildInfo buildInfo) {
@@ -216,7 +205,7 @@
         String moduleName = getTestModule();
         File gTestFile = null;
         try {
-            gTestFile = FileUtil.findFile(moduleName, mAbi, scanDirs.toArray(new File[] {}));
+            gTestFile = FileUtil.findFile(moduleName, getAbi(), scanDirs.toArray(new File[] {}));
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
@@ -226,7 +215,8 @@
             // search for it with a potential suffix (which is allowed).
             try {
                 File byBaseName =
-                        FileUtil.findFile(moduleName + ".*", mAbi, scanDirs.toArray(new File[] {}));
+                        FileUtil.findFile(
+                                moduleName + ".*", getAbi(), scanDirs.toArray(new File[] {}));
                 if (byBaseName != null && byBaseName.isFile()) {
                     gTestFile = byBaseName;
                 }
diff --git a/test_framework/com/android/tradefed/testtype/InstrumentationListener.java b/test_framework/com/android/tradefed/testtype/InstrumentationListener.java
index baafb76..6642922 100644
--- a/test_framework/com/android/tradefed/testtype/InstrumentationListener.java
+++ b/test_framework/com/android/tradefed/testtype/InstrumentationListener.java
@@ -135,7 +135,8 @@
                 super.testStarted(miss);
                 FailureDescription failure =
                         FailureDescription.create(
-                                "test did not run due to instrumentation issue.",
+                                "test did not run due to instrumentation issue. See run level "
+                                        + "error for reason.",
                                 FailureStatus.NOT_EXECUTED);
                 super.testFailed(miss, failure);
                 super.testEnded(miss, new HashMap<String, Metric>());
diff --git a/test_framework/com/android/tradefed/testtype/InstrumentationTest.java b/test_framework/com/android/tradefed/testtype/InstrumentationTest.java
index 75e82d7..5934799 100644
--- a/test_framework/com/android/tradefed/testtype/InstrumentationTest.java
+++ b/test_framework/com/android/tradefed/testtype/InstrumentationTest.java
@@ -16,13 +16,9 @@
 
 package com.android.tradefed.testtype;
 
-import static com.android.tradefed.testtype.coverage.CoverageOptions.Toolchain.CLANG;
-import static com.android.tradefed.testtype.coverage.CoverageOptions.Toolchain.GCOV;
-import static com.android.tradefed.testtype.coverage.CoverageOptions.Toolchain.JACOCO;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.common.base.Verify.verify;
 
 import com.android.ddmlib.IDevice;
 import com.android.ddmlib.Log;
@@ -37,14 +33,15 @@
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.metric.GcovCodeCoverageCollector;
 import com.android.tradefed.device.metric.IMetricCollector;
 import com.android.tradefed.device.metric.IMetricCollectorReceiver;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.BugreportCollector;
 import com.android.tradefed.result.CollectingTestListener;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.ITestLifeCycleReceiver;
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.result.TestResult;
 import com.android.tradefed.result.TestRunResult;
@@ -52,13 +49,10 @@
 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;
 import com.android.tradefed.util.AbiFormatter;
 import com.android.tradefed.util.ArrayUtil;
-import com.android.tradefed.util.JavaCodeCoverageFlusher;
 import com.android.tradefed.util.ListInstrumentationParser;
 import com.android.tradefed.util.ListInstrumentationParser.InstrumentationTarget;
-import com.android.tradefed.util.NativeCodeCoverageFlusher;
 import com.android.tradefed.util.StringEscapeUtils;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -98,8 +92,7 @@
     /** default timeout for tests collection */
     static final long TEST_COLLECTION_TIMEOUT_MS = 2 * 60 * 1000;
 
-    /** test run name for merging coverage measurements */
-    static final String MERGE_COVERAGE_MEASUREMENTS_TEST_NAME = "mergeCoverageMeasurements";
+    static final String RUN_TESTS_AS_USER_KEY = "RUN_TESTS_AS_USER";
 
     @Option(
         name = "package",
@@ -260,12 +253,12 @@
     )
     private boolean mCoverage = false;
 
+    @Deprecated
     @Option(
-        name = "merge-coverage-measurements",
-        description =
-                "Merge coverage measurements from all test runs into a single measurement before "
-                        + "logging."
-    )
+            name = "merge-coverage-measurements",
+            description =
+                    "Merge coverage measurements from all test runs into a single measurement "
+                            + "before logging.")
     private boolean mMergeCoverageMeasurements = false;
 
     @Deprecated
@@ -340,7 +333,7 @@
     private String mTestFilePathOnDevice = null;
 
     private ListInstrumentationParser mListInstrumentationParser = null;
-    private NativeCodeCoverageListener mNativeCoverageListener = null;
+    private GcovCodeCoverageCollector mNativeCoverageListener = null;
 
     private List<String> mExtraDeviceListener = new ArrayList<>();
 
@@ -649,12 +642,6 @@
         return mForceAbi;
     }
 
-    /** Sets the --merge-coverage-measurements option for testing. */
-    @VisibleForTesting
-    void setMergeCoverageMeasurements(boolean merge) {
-        mMergeCoverageMeasurements = merge;
-    }
-
     /** Sets the --rerun-from-file option. */
     public void setReRunUsingTestFile(boolean reRunUsingTestFile) {
         mReRunUsingTestFile = reRunUsingTestFile;
@@ -884,7 +871,8 @@
                     "Tests to run should not be set explicitly when --collect-tests-only is set.");
 
             // Use the actual listener to collect the tests, and print a error if this fails
-            Collection<TestDescription> collectedTests = collectTestsToRun(mRunner, listener);
+            Collection<TestDescription> collectedTests =
+                    collectTestsToRun(testInfo, mRunner, listener);
             if (collectedTests == null) {
                 CLog.e("Failed to collect tests for %s", mPackageName);
             } else {
@@ -897,7 +885,7 @@
         Collection<TestDescription> testsToRun = mTestsToRun;
         if (testsToRun == null) {
             // Don't notify the listener since it's not a real run.
-            testsToRun = collectTestsToRun(mRunner, null);
+            testsToRun = collectTestsToRun(testInfo, mRunner, null);
         }
 
         // Only set the debug flag after collecting tests.
@@ -908,34 +896,9 @@
             mRunner.addInstrumentationArg("coverage", "true");
         }
 
-        // Reruns do not create new listeners or clear coverage measurements.
+        // Reruns do not create new listeners.
         if (!mIsRerun) {
             listener = addBugreportListenerIfEnabled(listener);
-            listener = addJavaCoverageListenerIfEnabled(listener);
-            listener = addGcovCoverageListenerIfEnabled(listener);
-            listener = addClangCoverageListenerIfEnabled(listener);
-
-            // Clear coverage measurements on the device before running.
-            if (mConfiguration != null
-                    && mConfiguration.getCoverageOptions().isCoverageFlushEnabled()) {
-                CoverageOptions options = mConfiguration.getCoverageOptions();
-
-                if (options.getCoverageToolchains().contains(GCOV)
-                        || options.getCoverageToolchains().contains(CLANG)) {
-                    // Enable abd root on the device, otherwise the following commands will fail.
-                    verify(mDevice.enableAdbRoot(), "Failed to enable adb root.");
-
-                    NativeCodeCoverageFlusher flusher =
-                            new NativeCodeCoverageFlusher(mDevice, options.getCoverageProcesses());
-                    flusher.resetCoverage();
-                }
-
-                if (options.getCoverageToolchains().contains(JACOCO)) {
-                    JavaCodeCoverageFlusher flusher =
-                            new JavaCodeCoverageFlusher(mDevice, options.getCoverageProcesses());
-                    flusher.resetCoverage();
-                }
-            }
 
             // TODO: Convert to device-side collectors when possible.
             for (IMetricCollector collector : mCollectors) {
@@ -945,6 +908,9 @@
                     CLog.d(
                             "Initializing %s for instrumentation.",
                             collector.getClass().getCanonicalName());
+                    if (collector instanceof IConfigurationReceiver) {
+                        ((IConfigurationReceiver) collector).setConfiguration(mConfiguration);
+                    }
                     listener = collector.init(testInfo.getContext(), listener);
                 }
             }
@@ -957,19 +923,26 @@
 
         if (testsToRun == null) {
             // Failed to collect the tests or collection is off. Just try to run them all.
-            mDevice.runInstrumentationTests(mRunner, listener);
+            runInstrumentationTests(testInfo, mRunner, listener);
         } else if (!testsToRun.isEmpty()) {
             runWithRerun(testInfo, listener, testsToRun);
         } else {
             CLog.i("No tests expected for %s, skipping", mPackageName);
         }
+    }
 
-        // Merge coverage measurements after all tests have been run, but not inside the rerun
-        // itself since the merging will be handled by the caller.
-        if (!mIsRerun && mMergeCoverageMeasurements) {
-            listener.testRunStarted(MERGE_COVERAGE_MEASUREMENTS_TEST_NAME, 0);
-            listener.testRunEnded(0, new HashMap<String, Metric>());
+    private boolean runInstrumentationTests(
+            TestInformation testInfo,
+            IRemoteAndroidTestRunner runner,
+            ITestLifeCycleReceiver... receivers)
+            throws DeviceNotAvailableException {
+        if (testInfo != null && testInfo.properties().containsKey(RUN_TESTS_AS_USER_KEY)) {
+            return mDevice.runInstrumentationTestsAsUser(
+                    runner,
+                    Integer.parseInt(testInfo.properties().get(RUN_TESTS_AS_USER_KEY)),
+                    receivers);
         }
+        return mDevice.runInstrumentationTests(runner, receivers);
     }
 
     /**
@@ -991,61 +964,6 @@
     }
 
     /**
-     * Returns a listener that will collect coverage measurements, or the original {@code listener}
-     * if this feature is disabled.
-     */
-    ITestInvocationListener addJavaCoverageListenerIfEnabled(ITestInvocationListener listener) {
-        if (mConfiguration == null) {
-            return listener;
-        }
-        if (mConfiguration.getCoverageOptions().isCoverageEnabled()
-                && mConfiguration.getCoverageOptions().getCoverageToolchains().contains(JACOCO)) {
-            return new JavaCodeCoverageListener(
-                    getDevice(),
-                    mConfiguration.getCoverageOptions(),
-                    mMergeCoverageMeasurements,
-                    listener);
-        }
-        return listener;
-    }
-
-    /**
-     * Returns a listener that will collect gcov coverage measurements, or the original {@code
-     * listener} if this feature is disabled.
-     */
-    ITestInvocationListener addGcovCoverageListenerIfEnabled(ITestInvocationListener listener) {
-        if (mConfiguration == null) {
-            return listener;
-        }
-        if (mConfiguration.getCoverageOptions().isCoverageEnabled()
-                && mConfiguration.getCoverageOptions().getCoverageToolchains().contains(GCOV)) {
-            mNativeCoverageListener =
-                    new NativeCodeCoverageListener(
-                            getDevice(), mConfiguration.getCoverageOptions(), listener);
-            return mNativeCoverageListener;
-        }
-        return listener;
-    }
-
-    /**
-     * Returns a listener that will collect Clang coverage measurements, or the original {@code
-     * listener} if this feature is disabled.
-     */
-    ITestInvocationListener addClangCoverageListenerIfEnabled(ITestInvocationListener listener) {
-        if (mConfiguration == null) {
-            return listener;
-        }
-        if (mConfiguration.getCoverageOptions().isCoverageEnabled()
-                && mConfiguration.getCoverageOptions().getCoverageToolchains().contains(CLANG)) {
-            ClangCodeCoverageListener clangListener =
-                    new ClangCodeCoverageListener(getDevice(), listener);
-            clangListener.setConfiguration(mConfiguration);
-            return clangListener;
-        }
-        return listener;
-    }
-
-    /**
      * Execute the test run, but re-run incomplete tests individually if run fails to complete.
      *
      * @param listener the {@link ITestInvocationListener}
@@ -1067,7 +985,7 @@
                     getDevice().getProcessByName("system_server"));
         }
         instrumentationListener.setReportUnexecutedTests(mReportUnexecuted);
-        mDevice.runInstrumentationTests(mRunner, instrumentationListener);
+        runInstrumentationTests(testInfo, 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.
@@ -1168,7 +1086,9 @@
      * @throws DeviceNotAvailableException
      */
     private Collection<TestDescription> collectTestsToRun(
-            final IRemoteAndroidTestRunner runner, final ITestInvocationListener listener)
+            final TestInformation testInfo,
+            final IRemoteAndroidTestRunner runner,
+            final ITestInvocationListener listener)
             throws DeviceNotAvailableException {
         if (isRerunMode()) {
             Log.d(LOG_TAG, String.format("Collecting test info for %s on device %s",
@@ -1178,7 +1098,7 @@
             runner.setDebug(false);
             // try to collect tests multiple times, in case device is temporarily not available
             // on first attempt
-            Collection<TestDescription> tests = collectTestsAndRetry(runner, listener);
+            Collection<TestDescription> tests = collectTestsAndRetry(testInfo, runner, listener);
             // done with "logOnly" mode, restore proper test timeout before real test execution
             addTimeoutsToRunner(runner);
             runner.setTestCollection(false);
@@ -1198,7 +1118,9 @@
      */
     @VisibleForTesting
     Collection<TestDescription> collectTestsAndRetry(
-            final IRemoteAndroidTestRunner runner, final ITestInvocationListener listener)
+            final TestInformation testInfo,
+            final IRemoteAndroidTestRunner runner,
+            final ITestInvocationListener listener)
             throws DeviceNotAvailableException {
         boolean communicationFailure = false;
         for (int i=0; i < COLLECT_TESTS_ATTEMPTS; i++) {
@@ -1207,9 +1129,9 @@
             // We allow to override the ddmlib default timeout for collection of tests.
             runner.setMaxTimeToOutputResponse(mCollectTestTimeout, TimeUnit.MILLISECONDS);
             if (listener == null) {
-                instrResult = mDevice.runInstrumentationTests(runner, collector);
+                instrResult = runInstrumentationTests(testInfo, runner, collector);
             } else {
-                instrResult = mDevice.runInstrumentationTests(runner, collector, listener);
+                instrResult = runInstrumentationTests(testInfo, runner, collector, listener);
             }
             TestRunResult runResults = collector.getCurrentRunResults();
             if (!instrResult || !runResults.isRunComplete()) {
diff --git a/test_framework/com/android/tradefed/testtype/IsolatedHostTest.java b/test_framework/com/android/tradefed/testtype/IsolatedHostTest.java
index 337370e..ac89d5c 100644
--- a/test_framework/com/android/tradefed/testtype/IsolatedHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/IsolatedHostTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.tradefed.testtype;
 
+import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.build.IDeviceBuildInfo;
 import com.android.tradefed.config.Option;
@@ -205,8 +206,11 @@
      */
     private String compileClassPath() throws ClassNotFoundException {
         List<String> paths = new ArrayList<>();
-        IDeviceBuildInfo build = (IDeviceBuildInfo) mBuildInfo;
-        File testDir = build.getTestsDir();
+        File testDir = mBuildInfo.getFile(BuildInfoFileKey.TESTDIR_IMAGE);
+
+        if (!testDir.exists()) {
+            throw new IllegalArgumentException("Test directory not found, cannot proceed");
+        }
 
         // This is a relatively hacky way to get around the fact that we don't have a consistent
         // way to locate tradefed related jars in all environments, so instead we dyn link to that
diff --git a/test_framework/com/android/tradefed/testtype/JavaCodeCoverageListener.java b/test_framework/com/android/tradefed/testtype/JavaCodeCoverageListener.java
deleted file mode 100644
index 0076620..0000000
--- a/test_framework/com/android/tradefed/testtype/JavaCodeCoverageListener.java
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.testtype;
-
-import static com.google.common.base.Verify.verifyNotNull;
-import static com.google.common.io.Files.getNameWithoutExtension;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.ResultForwarder;
-import com.android.tradefed.testtype.coverage.CoverageOptions;
-import com.android.tradefed.util.FileUtil;
-import com.android.tradefed.util.JavaCodeCoverageFlusher;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Splitter;
-import com.google.common.collect.ImmutableList;
-
-import org.jacoco.core.tools.ExecFileLoader;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.List;
-
-/**
- * A {@link ResultForwarder} that will pull Java coverage measurements off of the device and log
- * them as test artifacts.
- */
-final class JavaCodeCoverageListener extends ResultForwarder {
-
-    public static final String MERGE_COVERAGE_MEASUREMENTS_TEST_NAME = "mergeCoverageMeasurements";
-    public static final String COVERAGE_MEASUREMENT_KEY = "coverageFilePath";
-    public static final String COVERAGE_DIRECTORY = "/data/misc/trace";
-    public static final String FIND_COVERAGE_FILES =
-            String.format("find %s -name '*.ec'", COVERAGE_DIRECTORY);
-
-    private final ITestDevice mDevice;
-    private final CoverageOptions mCoverageOptions;
-
-    private final boolean mMergeCoverageMeasurements;
-
-    private final ExecFileLoader mExecFileLoader = new ExecFileLoader();
-
-    private JavaCodeCoverageFlusher mFlusher;
-    private String mCurrentRunName;
-
-    public JavaCodeCoverageListener(
-            ITestDevice device,
-            CoverageOptions options,
-            boolean mergeMeasurements,
-            ITestInvocationListener... listeners) {
-        super(listeners);
-        mDevice = device;
-        mCoverageOptions = options;
-        mMergeCoverageMeasurements = mergeMeasurements;
-
-        mFlusher = new JavaCodeCoverageFlusher(device, options.getCoverageProcesses());
-    }
-
-    @VisibleForTesting
-    public void setCoverageFlusher(JavaCodeCoverageFlusher flusher) {
-        mFlusher = flusher;
-    }
-
-    @Override
-    public void testRunStarted(String runName, int testCount) {
-        super.testRunStarted(runName, testCount);
-        mCurrentRunName = runName;
-    }
-
-    @Override
-    public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
-        if (MERGE_COVERAGE_MEASUREMENTS_TEST_NAME.equals(mCurrentRunName)) {
-            // Log the merged runtime coverage measurement.
-            try {
-                File mergedMeasurements =
-                        FileUtil.createTempFile(
-                                "merged_runtime_coverage_",
-                                "." + LogDataType.COVERAGE.getFileExt());
-
-                mExecFileLoader.save(mergedMeasurements, false);
-
-                // Save the merged measurement as a test log.
-                logCoverageMeasurement("merged_runtime_coverage", mergedMeasurements);
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            } finally {
-                super.testRunEnded(elapsedTime, runMetrics);
-            }
-        } else {
-            // Get the path of the coverage measurement on the device.
-            Metric devicePathMetric = runMetrics.get(COVERAGE_MEASUREMENT_KEY);
-            if (devicePathMetric == null) {
-                super.testRunFailed("No Java code coverage measurement.");
-                super.testRunEnded(elapsedTime, runMetrics);
-                return;
-            }
-            String testCoveragePath = devicePathMetric.getMeasurements().getSingleString();
-            if (testCoveragePath == null) {
-                super.testRunFailed("No Java code coverage measurement.");
-                super.testRunEnded(elapsedTime, runMetrics);
-                return;
-            }
-
-            ImmutableList.Builder<String> devicePaths = ImmutableList.builder();
-            devicePaths.add(testCoveragePath);
-
-            try {
-                if (mCoverageOptions.isCoverageFlushEnabled()) {
-                    mFlusher.forceCoverageFlush();
-                }
-
-                // Find all .ec files in /data/misc/trace and pull them from the device as well.
-                String fileList = mDevice.executeShellCommand(FIND_COVERAGE_FILES);
-                devicePaths.addAll(Splitter.on('\n').omitEmptyStrings().split(fileList));
-
-                collectAndLogCoverageMeasurementsAsRoot(devicePaths.build());
-
-            } catch (DeviceNotAvailableException | IOException e) {
-                throw new RuntimeException(e);
-            } finally {
-                super.testRunEnded(elapsedTime, runMetrics);
-            }
-        }
-    }
-
-    private void logCoverageMeasurement(String name, File coverageFile) {
-        try (FileInputStreamSource source = new FileInputStreamSource(coverageFile, true)) {
-            testLog(name, LogDataType.COVERAGE, source);
-        }
-    }
-
-    private void collectAndLogCoverageMeasurementsAsRoot(List<String> devicePaths)
-            throws IOException, DeviceNotAvailableException {
-
-        // We enable root before pulling files off the device since the coverage file of the test
-        // process is written in its private directory which is otherwise inaccessible. Coverage
-        // files of other processes should be accessible without root. Note that we also restore
-        // root status to what it was after we're done to not interfere with subsequent tests that
-        // run on the device.
-        boolean wasRoot = mDevice.isAdbRoot();
-        if (!wasRoot && !mDevice.enableAdbRoot()) {
-            throw new RuntimeException(
-                    "Failed to enable root before pulling Java code coverage files off device");
-        }
-
-        try {
-            collectAndLogCoverageMeasurements(devicePaths);
-        } finally {
-            for (String devicePath : devicePaths) {
-                mDevice.deleteFile(devicePath);
-            }
-
-            if (!wasRoot && !mDevice.disableAdbRoot()) {
-                throw new RuntimeException(
-                        "Failed to disable root after pulling Java code coverage files off device");
-            }
-        }
-    }
-
-    private void collectAndLogCoverageMeasurements(List<String> devicePaths)
-            throws IOException, DeviceNotAvailableException {
-
-        for (String devicePath : devicePaths) {
-            File coverageFile = mDevice.pullFile(devicePath);
-            verifyNotNull(
-                    coverageFile, "Failed to pull the Java code coverage file from %s", devicePath);
-
-            // When merging, load the measurement data. Otherwise log the measurement
-            // immediately.
-            try {
-                if (mMergeCoverageMeasurements) {
-                    mExecFileLoader.load(coverageFile);
-                } else {
-                    logCoverageMeasurement(
-                            mCurrentRunName
-                                    + "_"
-                                    + getNameWithoutExtension(devicePath)
-                                    + "_runtime_coverage",
-                            coverageFile);
-                }
-            } finally {
-                FileUtil.deleteFile(coverageFile);
-            }
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/testtype/NativeCodeCoverageListener.java b/test_framework/com/android/tradefed/testtype/NativeCodeCoverageListener.java
deleted file mode 100644
index 54217ef..0000000
--- a/test_framework/com/android/tradefed/testtype/NativeCodeCoverageListener.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.testtype;
-
-import static com.google.common.base.Verify.verify;
-import static com.google.common.base.Verify.verifyNotNull;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.ResultForwarder;
-import com.android.tradefed.testtype.coverage.CoverageOptions;
-import com.android.tradefed.util.FileUtil;
-import com.android.tradefed.util.NativeCodeCoverageFlusher;
-import com.android.tradefed.util.TarUtil;
-import com.android.tradefed.util.ZipUtil;
-
-import com.google.common.collect.ImmutableList;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.HashMap;
-
-/**
- * A {@link ResultForwarder} that will pull native coverage measurements off of the device and log
- * them as test artifacts.
- */
-public final class NativeCodeCoverageListener extends ResultForwarder {
-
-    private static final String NATIVE_COVERAGE_DEVICE_PATH = "/data/misc/trace";
-    private static final String COVERAGE_TAR_PATH =
-            String.format("%s/coverage.tar", NATIVE_COVERAGE_DEVICE_PATH);
-
-    // Finds .gcda files in /data/misc/trace and compresses those files only. Stores the full
-    // path of the file on the device.
-    private static final String ZIP_COVERAGE_FILES_COMMAND =
-            String.format(
-                    "find %s -name '*.gcda' | tar -cvf %s -T -",
-                    NATIVE_COVERAGE_DEVICE_PATH, COVERAGE_TAR_PATH);
-
-    // Deletes .gcda files in /data/misc/trace.
-    private static final String DELETE_COVERAGE_FILES_COMMAND =
-            String.format("find %s -name '*.gcda' -delete", NATIVE_COVERAGE_DEVICE_PATH);
-
-    private final boolean mFlushCoverage;
-    private final ITestDevice mDevice;
-    private final NativeCodeCoverageFlusher mFlusher;
-    private boolean mCollectCoverageOnTestEnd = true;
-
-    private String mCurrentRunName;
-
-    public NativeCodeCoverageListener(ITestDevice device, ITestInvocationListener... listeners) {
-        super(listeners);
-        mDevice = device;
-        mFlushCoverage = false;
-        mFlusher = new NativeCodeCoverageFlusher(mDevice, ImmutableList.of());
-    }
-
-    public NativeCodeCoverageListener(
-            ITestDevice device,
-            CoverageOptions coverageOptions,
-            ITestInvocationListener... listeners) {
-        super(listeners);
-        mDevice = device;
-        mFlushCoverage = coverageOptions.isCoverageFlushEnabled();
-        mFlusher = new NativeCodeCoverageFlusher(mDevice, coverageOptions.getCoverageProcesses());
-    }
-
-    /**
-     * Sets whether to collect coverage on testRunEnded.
-     *
-     * <p>Set this to false during re-runs, otherwise each individual test re-run will collect
-     * coverage rather than having a single merged coverage result.
-     */
-    public void setCollectOnTestEnd(boolean collect) {
-        mCollectCoverageOnTestEnd = collect;
-    }
-
-    @Override
-    public void testRunStarted(String runName, int testCount) {
-        super.testRunStarted(runName, testCount);
-        mCurrentRunName = runName;
-    }
-
-    @Override
-    public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
-        try {
-            if (mCollectCoverageOnTestEnd) {
-                logCoverageMeasurements(mCurrentRunName);
-            }
-        } finally {
-            super.testRunEnded(elapsedTime, runMetrics);
-        }
-    }
-
-    /** Pulls native coverage measurements from the device and logs them. */
-    public void logCoverageMeasurements(String runName) {
-        File coverageTar = null;
-        File coverageZip = null;
-        try {
-            // Enable abd root on the device, otherwise the following commands will fail.
-            verify(mDevice.enableAdbRoot(), "Failed to enable adb root.");
-
-            // Flush cross-process coverage.
-            if (mFlushCoverage) {
-                mFlusher.forceCoverageFlush();
-            }
-
-            // Compress coverage measurements on the device before pulling.
-            mDevice.executeShellCommand(ZIP_COVERAGE_FILES_COMMAND);
-            coverageTar = mDevice.pullFile(COVERAGE_TAR_PATH);
-            verifyNotNull(
-                    coverageTar,
-                    "Failed to pull the native code coverage file %s",
-                    COVERAGE_TAR_PATH);
-            mDevice.deleteFile(COVERAGE_TAR_PATH);
-
-            coverageZip = convertTarToZip(coverageTar);
-
-            try (FileInputStreamSource source = new FileInputStreamSource(coverageZip, true)) {
-                testLog(runName + "_native_runtime_coverage", LogDataType.NATIVE_COVERAGE, source);
-            }
-
-            // Delete coverage files on the device.
-            mDevice.executeShellCommand(DELETE_COVERAGE_FILES_COMMAND);
-        } catch (DeviceNotAvailableException | IOException e) {
-            throw new RuntimeException(e);
-        } finally {
-            FileUtil.deleteFile(coverageTar);
-            FileUtil.deleteFile(coverageZip);
-        }
-    }
-
-    /**
-     * Converts a .tar file to a .zip file.
-     *
-     * @param tar the .tar file to convert
-     * @return a .zip file with the same contents
-     * @throws IOException
-     */
-    private File convertTarToZip(File tar) throws IOException {
-        File untarDir = null;
-        try {
-            untarDir = FileUtil.createTempDir("gcov_coverage");
-            TarUtil.unTar(tar, untarDir);
-            return ZipUtil.createZip(Arrays.asList(untarDir.listFiles()), "native_coverage");
-        } finally {
-            FileUtil.recursiveDelete(untarDir);
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java b/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
index 94e4aa6..055d7f7 100644
--- a/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
+++ b/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
@@ -169,7 +169,8 @@
                         FailureDescription.create(
                                         String.format(NO_BINARY_ERROR, cmd),
                                         FailureStatus.TEST_FAILURE)
-                                .setErrorIdentifier(InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
+                                .setErrorIdentifier(
+                                        InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
                 listener.testRunFailed(failure);
                 listener.testRunEnded(0L, new HashMap<String, Metric>());
             } else {
@@ -303,6 +304,9 @@
                 // Set one test command per shard
                 shard.mTestCommands.put(testName, cmd);
             }
+            // Copy the filters to each shard
+            shard.mExcludeFilters.addAll(mExcludeFilters);
+            shard.mIncludeFilters.addAll(mIncludeFilters);
         } catch (InstantiationException
                 | IllegalAccessException
                 | InvocationTargetException
diff --git a/test_framework/com/android/tradefed/testtype/host/CoverageMeasurementForwarder.java b/test_framework/com/android/tradefed/testtype/host/CoverageMeasurementForwarder.java
index 591d2d8..11ebdaa 100644
--- a/test_framework/com/android/tradefed/testtype/host/CoverageMeasurementForwarder.java
+++ b/test_framework/com/android/tradefed/testtype/host/CoverageMeasurementForwarder.java
@@ -36,7 +36,7 @@
 import java.util.HashMap;
 import java.util.List;
 
-/** A dummy test that fowards coverage measurements from the build provider to the logger. */
+/** A placeholder test that forwards coverage measurements from the build provider to the logger. */
 public final class CoverageMeasurementForwarder implements IRemoteTest {
 
     @Option(
diff --git a/test_framework/com/android/tradefed/testtype/junit4/JUnit4ResultForwarder.java b/test_framework/com/android/tradefed/testtype/junit4/JUnit4ResultForwarder.java
index be3fce4..0407f17 100644
--- a/test_framework/com/android/tradefed/testtype/junit4/JUnit4ResultForwarder.java
+++ b/test_framework/com/android/tradefed/testtype/junit4/JUnit4ResultForwarder.java
@@ -15,9 +15,13 @@
  */
 package com.android.tradefed.testtype.junit4;
 
+import com.android.tradefed.error.IHarnessException;
 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.TestDescription;
+import com.android.tradefed.result.error.ErrorIdentifier;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.LogAnnotation;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.MetricAnnotation;
 import com.android.tradefed.testtype.MetricTestCase.LogHolder;
@@ -51,18 +55,29 @@
     public void testFailure(Failure failure) throws Exception {
         Description description = failure.getDescription();
         if (description.getMethodName() == null) {
-            String trace = failure.getTrace();
-            if (failure.getException() instanceof CarryDnaeError) {
-                trace =
-                        StreamUtil.getStackTrace(
-                                ((CarryDnaeError) failure.getException())
-                                        .getDeviceNotAvailableException());
+            Throwable error = failure.getException();
+            String message = error.getMessage();
+            if (message == null) {
+                message = "Exception with no error message";
             }
-            // In case of exception in @BeforeClass, the method name will be null
-            mListener.testRunFailed(String.format("Failed with trace: %s", trace));
+            FailureDescription failureDesc =
+                    FailureDescription.create(message).setFailureStatus(FailureStatus.TEST_FAILURE);
+            if (error instanceof CarryDnaeError) {
+                error = ((CarryDnaeError) error).getDeviceNotAvailableException();
+            }
+            failureDesc.setCause(error);
+            if (error instanceof IHarnessException) {
+                ErrorIdentifier id = ((IHarnessException) error).getErrorId();
+                if (id != null) {
+                    failureDesc.setFailureStatus(id.status());
+                }
+                failureDesc.setErrorIdentifier(((IHarnessException) error).getErrorId());
+                failureDesc.setOrigin(((IHarnessException) error).getOrigin());
+            }
+            mListener.testRunFailed(failureDesc);
             // If the exception is ours thrown from before, rethrow it
-            if (failure.getException() instanceof CarryDnaeError) {
-                throw ((CarryDnaeError) failure.getException()).getDeviceNotAvailableException();
+            if (error instanceof CarryDnaeError) {
+                throw ((CarryDnaeError) error).getDeviceNotAvailableException();
             }
             return;
         }
diff --git a/test_framework/com/android/tradefed/testtype/mobly/MoblyBinaryHostTest.java b/test_framework/com/android/tradefed/testtype/mobly/MoblyBinaryHostTest.java
index 67cd44c..950f8df 100644
--- a/test_framework/com/android/tradefed/testtype/mobly/MoblyBinaryHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/mobly/MoblyBinaryHostTest.java
@@ -24,10 +24,13 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.FailureDescription;
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.targetprep.adb.AdbStopServerPreparer;
 import com.android.tradefed.testtype.IBuildReceiver;
 import com.android.tradefed.testtype.IDeviceTest;
@@ -37,6 +40,7 @@
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.PythonVirtualenvHelper;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.StreamUtil;
 
@@ -48,6 +52,7 @@
 import java.io.InputStream;
 import java.io.Writer;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -136,7 +141,11 @@
 
     @Override
     public final void run(ITestInvocationListener listener) {
-        List<File> parFilesList = findParFiles();
+        List<File> parFilesList = findParFiles(listener);
+        File venvDir = mBuildInfo.getFile("VIRTUAL_ENV");
+        if (venvDir != null) {
+            PythonVirtualenvHelper.activate(getRunUtil(), venvDir);
+        }
         for (File parFile : parFilesList) {
             // TODO(b/159365341): add a failure reporting for nonexistent binary.
             if (!parFile.exists()) {
@@ -153,9 +162,13 @@
                 reportLogs(getLogDir(), listener);
             }
         }
+        if (venvDir != null
+                && venvDir.getAbsolutePath().startsWith(System.getProperty("java.io.tmpdir"))) {
+            FileUtil.recursiveDelete(venvDir);
+        }
     }
 
-    private List<File> findParFiles() {
+    private List<File> findParFiles(ITestInvocationListener listener) {
         File testsDir = null;
         if (mBuildInfo instanceof IDeviceBuildInfo) {
             testsDir = ((IDeviceBuildInfo) mBuildInfo).getTestsDir();
@@ -169,6 +182,8 @@
                 res = FileUtil.findFile(testsDir, binaryName);
             }
             if (res == null) {
+                reportFailure(
+                        listener, binaryName, "Couldn't find Mobly test binary " + binaryName);
                 throw new RuntimeException(
                         String.format("Couldn't find a par file %s", binaryName));
             }
@@ -251,31 +266,43 @@
         InputStream inputStream = null;
         try {
             inputStream = new FileInputStream(yamlSummaryFile);
-            processYamlTestResults(inputStream, parser);
+            processYamlTestResults(inputStream, parser, listener, runName);
         } catch (FileNotFoundException ex) {
-            // TODO(b/159367088): report a test failure.
-            CLog.e("Fail processing test results: ", ex);
+            reportFailure(
+                    listener,
+                    runName,
+                    "Fail processing test results, result file not found.\n" + ex);
         } finally {
             StreamUtil.close(inputStream);
         }
     }
 
+    /**
+     * Parses Mobly test results and does result reporting.
+     *
+     * @param inputStream An InputStream object reading in Mobly test result file.
+     * @param parser An MoblyYamlResultParser object that processes Mobly test results.
+     * @param listener An ITestInvocationListener instance that does various reporting.
+     * @param runName str, the name of the Mobly test binary run.
+     */
     @VisibleForTesting
-    protected void processYamlTestResults(InputStream inputStream, MoblyYamlResultParser parser) {
+    protected void processYamlTestResults(
+            InputStream inputStream,
+            MoblyYamlResultParser parser,
+            ITestInvocationListener listener,
+            String runName) {
         try {
             parser.parse(inputStream);
         } catch (MoblyYamlResultHandlerFactory.InvalidResultTypeException
                 | IllegalAccessException
                 | InstantiationException ex) {
-            // TODO(b/159367088): report a test failure.
-            CLog.e("Failed to parse result file: %s", ex);
+            reportFailure(listener, runName, "Failed to parse the result file.\n" + ex);
         }
     }
 
     private void updateConfigFile() {
         InputStream inputStream = null;
         FileWriter fileWriter = null;
-        // TODO(b/159369745): clean up the tmp files created.
         File localConfigFile = new File(getLogDir(), "local_config.yaml");
         try {
             inputStream = new FileInputStream(mConfigFile);
@@ -334,6 +361,15 @@
         return mLogDir;
     }
 
+    private void reportFailure(
+            ITestInvocationListener listener, String runName, String errorMessage) {
+        listener.testRunStarted(runName, 0);
+        FailureDescription description =
+                FailureDescription.create(errorMessage, FailureStatus.TEST_FAILURE);
+        listener.testRunFailed(description);
+        listener.testRunEnded(0L, new HashMap<String, Metric>());
+    }
+
     @VisibleForTesting
     String getLogDirAbsolutePath() {
         return getLogDir().getAbsolutePath();
@@ -348,6 +384,8 @@
     protected String[] buildCommandLineArray(String filePath) {
         List<String> commandLine = new ArrayList<>();
         commandLine.add(filePath);
+        // TODO(b/166468397): some test binaries are actually a wrapper of Mobly runner and need --
+        //  to separate Python options.
         commandLine.add("--");
         if (getConfigPath() != null) {
             commandLine.add("--config=" + getConfigPath());
diff --git a/test_framework/com/android/tradefed/testtype/rust/RustBinaryHostTest.java b/test_framework/com/android/tradefed/testtype/rust/RustBinaryHostTest.java
index 42187c5..9723fe9 100644
--- a/test_framework/com/android/tradefed/testtype/rust/RustBinaryHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/rust/RustBinaryHostTest.java
@@ -38,7 +38,6 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -128,42 +127,70 @@
     }
 
     private void runSingleRustFile(ITestInvocationListener listener, File file) {
+        CLog.d("Run single Rust File: %s", file.getAbsolutePath());
+        // Rust binary does not support multiple inclusion filters,
+        // so we run the test once for each include filter.
+        List<String> includeFilters = getListOfIncludeFilters();
+
+        // Call with --list once per include filter to add up testCount.
+        // Duplicated test cases selected by different include filters should not be counted.
+        Set<String> foundTests = new HashSet<>();
+        for (String filter : includeFilters) {
+            countTests(file, filter, foundTests);
+        }
+        int testCount = foundTests.size();
+        CLog.d("Total test count: %d", testCount);
+        long startTimeMs = System.currentTimeMillis();
+        listener.testRunStarted(file.getName(), testCount, 0, startTimeMs);
+        for (String filter : includeFilters) {
+            try {
+                runTestWithFilter(listener, file, filter);
+            } catch (IOException e) {
+                listener.testRunFailed(e.getMessage());
+                long testTimeMs = System.currentTimeMillis() - startTimeMs;
+                listener.testRunEnded(testTimeMs, new HashMap<String, Metric>());
+                throw new RuntimeException(e);
+            }
+        }
+        long testTimeMs = System.currentTimeMillis() - startTimeMs;
+        listener.testRunEnded(testTimeMs, new HashMap<String, Metric>());
+    }
+
+    private void countTests(File file, String filter, Set<String> foundTests) {
+        CLog.d("Count with filter '%s' for Rust File: %s", filter, file.getAbsolutePath());
         List<String> commandLine = new ArrayList<>();
         commandLine.add(file.getAbsolutePath());
-        CLog.d("Run single Rust File: " + file.getAbsolutePath());
-
-        // Add all the other options and include/exclude filters.
         commandLine.addAll(mTestOptions);
-        addFiltersToArgs(commandLine);
+        addFiltersToArgs(commandLine, filter);
 
         List<String> listCommandLine = new ArrayList<>(commandLine);
         listCommandLine.add("--list");
         CommandResult listResult =
                 getRunUtil()
                         .runTimedCmdSilently(mTestTimeout, listCommandLine.toArray(new String[0]));
-        int testCount = 0;
         // TODO: Do we want to handle non-standard test harnesses without a
         // --list param? Currently we will report 0 tests, which will cause an
         // overall failure, but we don't know how to parse arbitrary test
         // harness results.
         if (listResult.getStatus() == CommandStatus.SUCCESS) {
-            try {
-                testCount = parseTestListCount(listResult.getStdout().split("\n"));
-            } catch (ParseException e) {
-                CLog.w("Parsing test list failed: %s", e.getMessage());
-            }
+            collectTestLines(listResult.getStdout().split("\n"), foundTests);
         } else {
             CLog.w(
                     "Could not run command '%s' to get test list.",
                     String.join(" ", listCommandLine));
         }
+    }
 
+    private void runTestWithFilter(ITestInvocationListener listener, File file, String filter)
+            throws IOException {
         String runName = file.getName();
-        long startTimeMs = System.currentTimeMillis();
-        listener.testRunStarted(runName, testCount, 0, startTimeMs);
+        List<String> commandLine = new ArrayList<>();
+        commandLine.add(file.getAbsolutePath());
+        commandLine.addAll(mTestOptions);
+        addFiltersToArgs(commandLine, filter);
+        CLog.d("Running test with filter '%s'", filter);
         CommandResult result =
                 getRunUtil().runTimedCmd(mTestTimeout, commandLine.toArray(new String[0]));
-        long testTimeMs = System.currentTimeMillis() - startTimeMs;
         if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
             String message =
                     String.format(
@@ -185,17 +212,15 @@
                         String.format(RUST_LOG_STDERR_FORMAT, runName), LogDataType.TEXT, data);
             }
             String[] lines = result.getStdout().split("\n");
-            new RustTestResultParser(listener, runName).processNewLines(lines);
+            RustTestResultParser parser = new RustTestResultParser(listener, runName);
+            parser.processNewLines(lines);
+            parser.done();
         } catch (RuntimeException e) {
             listener.testRunFailed(
                     String.format("Failed to parse the rust test output: %s", e.getMessage()));
             CLog.e(e);
-        } catch (IOException e) {
-            listener.testRunFailed(e.getMessage());
-            throw new RuntimeException(e);
         } finally {
             FileUtil.deleteFile(resultFile);
-            listener.testRunEnded(testTimeMs, new HashMap<String, Metric>());
         }
     }
 
diff --git a/test_framework/com/android/tradefed/testtype/rust/RustBinaryTest.java b/test_framework/com/android/tradefed/testtype/rust/RustBinaryTest.java
index a35b722..7ff34c3 100644
--- a/test_framework/com/android/tradefed/testtype/rust/RustBinaryTest.java
+++ b/test_framework/com/android/tradefed/testtype/rust/RustBinaryTest.java
@@ -16,10 +16,6 @@
 
 package com.android.tradefed.testtype.rust;
 
-import static com.android.tradefed.testtype.coverage.CoverageOptions.Toolchain.GCOV;
-
-import static com.google.common.base.Verify.verify;
-
 import com.android.ddmlib.FileListingService;
 import com.android.ddmlib.IShellOutputReceiver;
 import com.android.tradefed.config.IConfiguration;
@@ -33,13 +29,12 @@
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.testtype.IDeviceTest;
-import com.android.tradefed.testtype.NativeCodeCoverageListener;
-import com.android.tradefed.testtype.coverage.CoverageOptions;
-import com.android.tradefed.util.NativeCodeCoverageFlusher;
 
 import java.io.File;
-import java.text.ParseException;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 /** A Test that runs a rust binary on given device. */
@@ -155,38 +150,45 @@
             final IShellOutputReceiver resultParser,
             final String fullPath)
             throws DeviceNotAvailableException {
-        // TODO(chh): add rerun support
         CLog.d("RustBinaryTest runTest: " + fullPath);
-        // TODO(chh): add LD_LIBRARY_PATH
-        String cmd;
-        if (getCoverageOptions().isCoverageEnabled()) {
-            cmd = "GCOV_PREFIX=/data/misc/trace/testcoverage " + fullPath;
-        } else {
-            cmd = fullPath;
-        }
-        cmd = addFiltersToCommand(cmd);
+        String cmd = fullPath;
 
-        int testCount = 0;
-        try {
-            String[] testList = testDevice.executeShellCommand(cmd + " --list").split("\n");
-            testCount = parseTestListCount(testList);
-        } catch (DeviceNotAvailableException e) {
-            CLog.e("Could not retrieve tests list from device: %s", e.getMessage());
-            throw e;
-        } catch (ParseException e) {
-            CLog.w("Parsing test list failed: %s", e.getMessage());
+        // Rust binary does not support multiple inclusion filters,
+        // so we run the test once for each include filter.
+        List<String> includeFilters = getListOfIncludeFilters();
+
+        // Call with --list once per include filter to add up testCount.
+        // Duplicated test cases selected by different include filters should not be counted.
+        Set<String> foundTests = new HashSet<>();
+        for (String filter : includeFilters) {
+            String newCmd = addFiltersToCommand(cmd, filter);
+            try {
+                String[] testList = testDevice.executeShellCommand(newCmd + " --list").split("\n");
+                collectTestLines(testList, foundTests);
+            } catch (DeviceNotAvailableException e) {
+                CLog.e("Could not retrieve tests list from device: %s", e.getMessage());
+                throw e;
+            }
         }
+        int testCount = foundTests.size();
+        CLog.d("Total test count: %d", testCount);
         long startTimeMs = System.currentTimeMillis();
         listener.testRunStarted(new File(fullPath).getName(), testCount, 0, startTimeMs);
-        try {
-            testDevice.executeShellCommand(
-                    cmd, resultParser, mTestTimeout, TimeUnit.MILLISECONDS, 0 /* retryAttempts */);
-        } catch (DeviceNotAvailableException e) {
-            listener.testRunFailed(String.format("Device not available: %s", e.getMessage()));
-        } finally {
-            listener.testRunEnded(
-                    System.currentTimeMillis() - startTimeMs, new HashMap<String, Metric>());
+        for (String filter : includeFilters) {
+            String newCmd = addFiltersToCommand(cmd, filter);
+            try {
+                testDevice.executeShellCommand(
+                        newCmd,
+                        resultParser,
+                        mTestTimeout,
+                        TimeUnit.MILLISECONDS,
+                        0 /* retryAttempts */);
+            } catch (DeviceNotAvailableException e) {
+                listener.testRunFailed(String.format("Device not available: %s", e.getMessage()));
+            }
         }
+        long testTimeMs = System.currentTimeMillis() - startTimeMs;
+        listener.testRunEnded(testTimeMs, new HashMap<String, Metric>());
     }
 
     private void wrongTestPath(String msg, String testPath, ITestInvocationListener listener) {
@@ -212,56 +214,10 @@
             return;
         }
 
-        // Insert the coverage listener if code coverage collection is enabled.
-        listener = addNativeCoverageListenerIfEnabled(listener);
-        NativeCodeCoverageFlusher flusher =
-                new NativeCodeCoverageFlusher(mDevice, getCoverageOptions().getCoverageProcesses());
-
-        if (getCoverageOptions().isCoverageEnabled()) {
-            // Enable abd root on the device, otherwise the following commands will fail.
-            // TODO(b/159843590): Restore adb root state later.
-            verify(mDevice.enableAdbRoot(), "Failed to enable adb root.");
-
-            flusher.resetCoverage();
-
-            // Clang will no longer create directories that are part of the GCOV_PREFIX
-            // environment variable. Force create the /data/misc/trace/testcoverage dir to
-            // prevent "No such file or directory" errors when writing test coverage to disk.
-            mDevice.executeShellCommand("mkdir /data/misc/trace/testcoverage");
-        }
-
         CLog.d("To run tests in directory " + testPath);
 
         if (!doRunAllTestsInSubdirectory(testPath, mDevice, listener)) {
             wrongTestPath("No test found under ", testPath, listener);
         }
     }
-
-    /**
-     * Returns the {@link CoverageOptions} for this test, if it exists. Otherwise returns a default
-     * {@link CoverageOptions} object with all coverage disabled.
-     */
-    protected CoverageOptions getCoverageOptions() {
-        if (mConfiguration != null) {
-            return mConfiguration.getCoverageOptions();
-        }
-        return new CoverageOptions();
-    }
-
-    /**
-     * Adds a listener to pull native code coverage measurements from the device after the test is
-     * complete if coverage is enabled, otherwise returns the same listener.
-     *
-     * @param listener the current chain of listeners
-     * @return a native coverage listener if coverage is enabled, otherwise the original listener
-     */
-    private ITestInvocationListener addNativeCoverageListenerIfEnabled(
-            ITestInvocationListener listener) {
-        CoverageOptions options = getCoverageOptions();
-
-        if (options.isCoverageEnabled() && options.getCoverageToolchains().contains(GCOV)) {
-            return new NativeCodeCoverageListener(mDevice, options, listener);
-        }
-        return listener;
-    }
 }
diff --git a/test_framework/com/android/tradefed/testtype/rust/RustTestBase.java b/test_framework/com/android/tradefed/testtype/rust/RustTestBase.java
index aa84c00..1e507dd 100644
--- a/test_framework/com/android/tradefed/testtype/rust/RustTestBase.java
+++ b/test_framework/com/android/tradefed/testtype/rust/RustTestBase.java
@@ -25,12 +25,11 @@
 
 import com.google.common.annotations.VisibleForTesting;
 
-import java.text.ParseException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
-import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /** Base class of RustBinaryHostTest and RustBinaryTest */
@@ -51,12 +50,10 @@
             isTimeVal = true)
     protected long mTestTimeout = 20 * 1000L; // milliseconds
 
-    @Option(
-            name = "include-filter",
-            description = "A substr filter of the test names to run; only the first one is used.")
+    @Option(name = "include-filter", description = "A substr filter of test case names to run.")
     private Set<String> mIncludeFilters = new LinkedHashSet<>();
 
-    @Option(name = "exclude-filter", description = "A substr filter of the test names to skip.")
+    @Option(name = "exclude-filter", description = "A substr filter of test case names to skip.")
     private Set<String> mExcludeFilters = new LinkedHashSet<>();
 
     // A wrapper that can be redefined in unit tests to create a (mocked) result parser.
@@ -65,35 +62,6 @@
         return new RustTestResultParser(listener, runName);
     }
 
-    /**
-     * Parse and return the number of tests from the list of tests from a Rust test harness.
-     *
-     * @param testList the output of the Rust test list (output of `test_binary --list`).
-     */
-    protected static int parseTestListCount(String[] testList) throws ParseException {
-        // Get the number of tests we are running, assuming this is a standard
-        // rust test harness. If this isn't, this command will fail and we will
-        // report 0 tests, which is fine.
-        int testCount = 0;
-        if (testList.length > 0) {
-            Matcher matcher = TEST_LIST_PATTERN.matcher(testList[testList.length - 1]);
-            if (matcher.matches()) {
-                testCount = Integer.parseInt(matcher.group(1));
-            } else {
-                throw new ParseException(
-                        "Could not match total test/benchmark count output. "
-                                + "Does this test use the standard Rust test harness?",
-                        0);
-            }
-        } else {
-            throw new ParseException(
-                    "Test did not return any output with --list argument. "
-                            + "Does this test use the standard Rust test harness?",
-                    0);
-        }
-        return testCount;
-    }
-
     /** {@inheritDoc} */
     @Override
     public void addIncludeFilter(String filter) {
@@ -142,31 +110,48 @@
         return mExcludeFilters;
     }
 
-    private void checkMultipleIncludeFilters() {
-        if (mIncludeFilters.size() > 1) {
-            CLog.e("Found multiple include filters; all except the 1st are ignored.");
+    /** Find test case names in testList and add them into foundTests. */
+    protected static void collectTestLines(String[] testList, Set<String> foundTests) {
+        // Rust test --list returns "testName: test" for each test.
+        int counter = 0;
+        for (String line : testList) {
+            if (line.endsWith(": test")) {
+                counter++;
+                foundTests.add(line);
+            }
         }
+        CLog.d("Found %d tests", counter);
     }
 
-    protected void addFiltersToArgs(List<String> args) {
-        checkMultipleIncludeFilters();
-        for (String s : mIncludeFilters) {
-            args.add(s);
+    /** Convert TestBinaryName#TestCaseName to TestCaseName for Rust Test. */
+    protected String cleanFilter(String filter) {
+        return filter.replaceFirst(".*#", "");
+    }
+
+    protected List<String> getListOfIncludeFilters() {
+        if (mIncludeFilters.isEmpty()) {
+            // Run test only once without any include filter.
+            return new ArrayList<String>(Arrays.asList(""));
+        }
+        return new ArrayList<String>(mIncludeFilters);
+    }
+
+    protected void addFiltersToArgs(List<String> args, String filter) {
+        if (!"".equals(filter)) {
+            args.add(cleanFilter(filter));
         }
         for (String s : mExcludeFilters) {
             args.add("--skip");
-            args.add(s);
+            args.add(cleanFilter(s));
         }
     }
 
-    protected String addFiltersToCommand(String cmd) {
-        if (!mIncludeFilters.isEmpty()) {
-            checkMultipleIncludeFilters();
-            cmd += " " + String.join(" ", mIncludeFilters);
+    protected String addFiltersToCommand(String cmd, String filter) {
+        List<String> args = new ArrayList<>();
+        addFiltersToArgs(args, filter);
+        if (args.isEmpty()) {
+            return cmd;
         }
-        if (!mExcludeFilters.isEmpty()) {
-            cmd += " --skip " + String.join(" --skip ", mExcludeFilters);
-        }
-        return cmd;
+        return cmd + " " + String.join(" ", args);
     }
 }
diff --git a/test_framework/com/android/tradefed/testtype/rust/RustTestResultParser.java b/test_framework/com/android/tradefed/testtype/rust/RustTestResultParser.java
index 9807a5d..438533e 100644
--- a/test_framework/com/android/tradefed/testtype/rust/RustTestResultParser.java
+++ b/test_framework/com/android/tradefed/testtype/rust/RustTestResultParser.java
@@ -90,34 +90,15 @@
         mTestResultCache = new HashMap<>();
     }
 
-    /**
-     * Process Rust unittest output and report parsed results.
-     *
-     * <p>This method should be called only once with the full output, unlike the base method in
-     * {@link MultiLineReceiver}.
-     */
+    /** Process Rust unittest output. */
     @Override
     public void processNewLines(String[] lines) {
-        try {
-            boolean hasContent = false;
-            for (String line : lines) {
-                hasContent |= line.length() > 0;
-                if (lineMatchesPattern(line, COMPLETE_PATTERN)) {
-                    reportToListeners(line);
-                    return;
-                }
-                if (lineMatchesPattern(line, RUST_ONE_LINE_RESULT)) {
-                    mCurrentTestName = mCurrentMatcher.group(1);
-                    mCurrentTestStatus = mCurrentMatcher.group(2);
-                    reportTestResult();
-                }
+        for (String line : lines) {
+            if (lineMatchesPattern(line, RUST_ONE_LINE_RESULT)) {
+                mCurrentTestName = mCurrentMatcher.group(1);
+                mCurrentTestStatus = mCurrentMatcher.group(2);
+                reportTestResult();
             }
-            if (hasContent) {
-                throw new Exception(
-                        String.format("Missing summary line:\n%s\n", String.join("\n", lines)));
-            }
-        } catch (Exception e) {
-            throw new RuntimeException("Failed to parse Rust test result\n" + e);
         }
     }
 
@@ -144,7 +125,8 @@
     }
 
     /** Send recorded test results to all listeners. */
-    private void reportToListeners(String line) {
+    @Override
+    public void done() {
         for (ITestInvocationListener listener : mListeners) {
             for (Entry<TestDescription, String> test : mTestResultCache.entrySet()) {
                 listener.testStarted(test.getKey());
diff --git a/test_framework/com/android/tradefed/util/PythonVirtualenvHelper.java b/test_framework/com/android/tradefed/util/PythonVirtualenvHelper.java
new file mode 100644
index 0000000..35c4190
--- /dev/null
+++ b/test_framework/com/android/tradefed/util/PythonVirtualenvHelper.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util;
+
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.io.File;
+import java.util.stream.Stream;
+
+/** A helper class for activating Python 3 virtual environment. */
+public class PythonVirtualenvHelper {
+
+    private static final String PATH = "PATH";
+    private static final String PYTHONHOME = "PYTHONHOME";
+    private static final String PYTHONPATH = "PYTHONPATH";
+    public static final String VIRTUAL_ENV = "VIRTUAL_ENV";
+
+    /**
+     * Gets python bin directory path.
+     *
+     * <p>This method will check the directory existence.
+     *
+     * @return str, the path to the python bin directory in venv.
+     * @throws NullPointerException if arg virtualenvPath is null.
+     * @throws RuntimeException if /path/to/venv/bin does not exist.
+     */
+    public static String getPythonBinDir(String virtualenvPath) {
+        if (virtualenvPath == null) {
+            throw new NullPointerException(
+                    "Path to the Python virtual environment should not be null");
+        }
+        File res = new File(virtualenvPath, "bin");
+        if (!res.exists()) {
+            throw new RuntimeException("Invalid python virtualenv path " + res.getAbsolutePath());
+        }
+        return res.getAbsolutePath();
+    }
+
+    /**
+     * Activate virtualenv for a RunUtil.
+     *
+     * @param runUtil an utility object for running virtualenv activation commands.
+     * @param virtualenvDir a File object representing the created virtualenv directory.
+     */
+    public static void activate(IRunUtil runUtil, File virtualenvDir) {
+        activate(runUtil, virtualenvDir.getAbsolutePath());
+    }
+
+    /**
+     * Activate virtualenv for a RunUtil.
+     *
+     * <p>This method will check for python bin directory existence
+     *
+     * @param runUtil an utility object for running virtualenv activation commands.
+     * @param virtualenvPath the path to the created virtualenv directory.
+     */
+    public static void activate(IRunUtil runUtil, String virtualenvPath) {
+        String pythonBinDir = getPythonBinDir(virtualenvPath);
+        String separater = ":";
+        String pythonPath =
+                getPackageInstallLocation(runUtil, virtualenvPath)
+                        + separater
+                        + System.getenv(PYTHONPATH);
+        runUtil.setEnvVariable(PATH, pythonBinDir + separater + System.getenv().get(PATH));
+        runUtil.setEnvVariable(VIRTUAL_ENV, virtualenvPath);
+        runUtil.setEnvVariable(PYTHONPATH, pythonPath);
+        runUtil.unsetEnvVariable(PYTHONHOME);
+        CLog.d("Activating virtual environment:");
+        CLog.d("%s: %s", PATH, pythonBinDir + separater + System.getenv().get(PATH));
+        CLog.d("%s: %s", VIRTUAL_ENV, virtualenvPath);
+        CLog.d("%s: %s", PYTHONPATH, pythonPath);
+    }
+
+    /**
+     * Gets the absolute path to the pip3 binary in the given venv directory.
+     *
+     * @param virtualenvPath the path to the venv directory.
+     * @return a string representing the absolute path to the pip3 binary.
+     */
+    private static String getPipPath(String virtualenvPath) {
+        File pipFile = new File(PythonVirtualenvHelper.getPythonBinDir(virtualenvPath), "pip3");
+        return pipFile.getAbsolutePath();
+    }
+
+    /**
+     * Gets python package install location.
+     *
+     * <p>This method will call /path/to/venv/bin/pip3 show pip and parse out package location from
+     * stdout output.
+     *
+     * @param runUtil an utility object for running for running commands.
+     * @param virtualenvPath the path to the created virtualenv directory.
+     * @return a string representing the absolute path to the location where Python packages are
+     *     installed.
+     */
+    private static String getPackageInstallLocation(IRunUtil runUtil, String virtualenvPath) {
+        CommandResult result =
+                runUtil.runTimedCmd(60000, getPipPath(virtualenvPath), "show", "pip");
+        if (result.getStatus() != CommandStatus.SUCCESS) {
+            throw new RuntimeException(
+                    String.format(
+                            "Fail to run command: %s show pip.\nStatus:%s\nStdout:%s\nStderr:%s",
+                            getPipPath(virtualenvPath),
+                            result.getStatus(),
+                            result.getStdout(),
+                            result.getStderr()));
+        }
+        String stdout = result.getStdout();
+        String[] lines = stdout.split("\n");
+        String locationLine =
+                Stream.of(lines).filter(x -> x.startsWith("Location")).findFirst().orElse("");
+        return locationLine.split(" ")[1];
+    }
+}
diff --git a/test_result_interfaces/com/android/tradefed/result/MultiFailureDescription.java b/test_result_interfaces/com/android/tradefed/result/MultiFailureDescription.java
index 5124910..b58972a 100644
--- a/test_result_interfaces/com/android/tradefed/result/MultiFailureDescription.java
+++ b/test_result_interfaces/com/android/tradefed/result/MultiFailureDescription.java
@@ -49,7 +49,11 @@
      * @return The current {@link MultiFailureDescription}.
      */
     public MultiFailureDescription addFailure(FailureDescription failure) {
-        mFailures.add(failure);
+        if (failure instanceof MultiFailureDescription) {
+            addMultiFailures(((MultiFailureDescription) failure).getFailures());
+        } else {
+            mFailures.add(failure);
+        }
         return this;
     }
 
diff --git a/test_result_interfaces/com/android/tradefed/result/error/DeviceErrorIdentifier.java b/test_result_interfaces/com/android/tradefed/result/error/DeviceErrorIdentifier.java
deleted file mode 100644
index c3ef3a5..0000000
--- a/test_result_interfaces/com/android/tradefed/result/error/DeviceErrorIdentifier.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.result.error;
-
-import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
-
-/** Error Identifiers from Device errors and device reported errors. */
-public enum DeviceErrorIdentifier implements ErrorIdentifier {
-
-    // ********************************************************************************************
-    // Device Errors: 30_001 ~ 40_000
-    // ********************************************************************************************
-    APK_INSTALLATION_FAILED(30_001, FailureStatus.UNSET),
-
-    AAPT_PARSER_FAILED(30_050, FailureStatus.UNSET),
-
-    SHELL_COMMAND_ERROR(30_100, FailureStatus.UNSET),
-    DEVICE_UNEXPECTED_RESPONSE(30_101, FailureStatus.UNSET),
-
-    INSTRUMENATION_CRASH(30_200, FailureStatus.UNSET),
-
-    FAILED_TO_LAUNCH_GCE(30_500, FailureStatus.LOST_SYSTEM_UNDER_TEST),
-    FAILED_TO_CONNECT_TO_GCE(30_501, FailureStatus.LOST_SYSTEM_UNDER_TEST),
-    ERROR_AFTER_FLASHING(30_502, FailureStatus.LOST_SYSTEM_UNDER_TEST),
-
-    DEVICE_UNAVAILABLE(30_750, FailureStatus.LOST_SYSTEM_UNDER_TEST),
-    DEVICE_UNRESPONSIVE(30_751, FailureStatus.LOST_SYSTEM_UNDER_TEST);
-
-    private final long code;
-    private final FailureStatus status;
-
-    DeviceErrorIdentifier(int code, FailureStatus status) {
-        this.code = code;
-        this.status = status;
-    }
-
-    @Override
-    public long code() {
-        return code;
-    }
-
-    @Override
-    public FailureStatus status() {
-        return status;
-    }
-}
diff --git a/test_result_interfaces/com/android/tradefed/result/error/InfraErrorIdentifier.java b/test_result_interfaces/com/android/tradefed/result/error/InfraErrorIdentifier.java
deleted file mode 100644
index 3001231..0000000
--- a/test_result_interfaces/com/android/tradefed/result/error/InfraErrorIdentifier.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.result.error;
-
-import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
-
-/** Error Identifiers from Trade Federation infra, and dependent infra (like Build infra). */
-public enum InfraErrorIdentifier implements ErrorIdentifier {
-
-    // ********************************************************************************************
-    // Infra: 10_001 ~ 20_000
-    // ********************************************************************************************
-    // 10_001 - 10_500: General errors
-    ARTIFACT_NOT_FOUND(10_001, FailureStatus.INFRA_FAILURE),
-    FAIL_TO_CREATE_FILE(10_002, FailureStatus.INFRA_FAILURE),
-
-    // 10_501 - 11_000: Build, Artifacts download related errors
-    ARTIFACT_REMOTE_PATH_NULL(10_501, FailureStatus.INFRA_FAILURE),
-    ARTIFACT_UNSUPPORTED_PATH(10_502, FailureStatus.INFRA_FAILURE),
-    ARTIFACT_DOWNLOAD_ERROR(10_503, FailureStatus.INFRA_FAILURE),
-
-    // 11_001 - 11_500: environment issues: For example: lab wifi
-    WIFI_FAILED_CONNECT(11_001, FailureStatus.UNSET), // TODO: switch to dependency_issue
-
-    // 12_000 - 12_100: Test issues detected by infra
-    EXPECTED_TESTS_MISMATCH(12_000, FailureStatus.TEST_FAILURE),
-
-    UNDETERMINED(20_000, FailureStatus.UNSET);
-
-    private final long code;
-    private final FailureStatus status;
-
-    InfraErrorIdentifier(int code, FailureStatus status) {
-        this.code = code;
-        this.status = status;
-    }
-
-    @Override
-    public long code() {
-        return code;
-    }
-
-    @Override
-    public FailureStatus status() {
-        return status;
-    }
-}
diff --git a/tests/Android.bp b/tests/Android.bp
index 7d1334a..caf95bb 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -12,6 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+java_library_host {
+    name: "tradefed-test-protos",
+    srcs: ["res/**/*.proto"],
+    libs: [
+        "libprotobuf-java-full",
+    ],
+    proto: {
+        include_dirs: ["external/protobuf/src"],
+        type: "full",
+    }
+}
+
 tradefed_java_library_host {
     name: "tradefed-tests",
     defaults: ["tradefed_errorprone_defaults"],
@@ -30,6 +42,7 @@
         "easymock",
         "objenesis",
         "mockito",
+        "tradefed-test-protos",
     ],
     libs: [
         "tradefed",
diff --git a/tests/res/proto/proto_util_test.proto b/tests/res/proto/proto_util_test.proto
new file mode 100644
index 0000000..a593326
--- /dev/null
+++ b/tests/res/proto/proto_util_test.proto
@@ -0,0 +1,38 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+option java_package = "com.android.tradefed.util.test";
+option java_outer_classname = "ProtoUtilTestProto";
+
+// A proto for com.android.tradefed.util.ProtoUtilTest.
+message TestMessage {
+    string string_field = 1;
+    int64 int_field = 2;
+    repeated string repeated_string_field = 3;
+
+    message SubMessage {
+        int64 int_field = 1;
+        repeated string repeated_string_field = 2;
+    }
+
+    SubMessage message_field = 4;
+    repeated SubMessage repeated_message_field = 5;
+
+    oneof oneof_field {
+        string oneof_string_field = 6;
+        SubMessage oneof_message_field = 7;
+    }
+}
diff --git a/tests/res/testconfigs/yaml/not-target-preparer.tf_yaml b/tests/res/testconfigs/yaml/not-target-preparer.tf_yaml
new file mode 100644
index 0000000..610b637
--- /dev/null
+++ b/tests/res/testconfigs/yaml/not-target-preparer.tf_yaml
@@ -0,0 +1,16 @@
+description: "Human friendly description of the test"
+
+# pre_setup_action will run before the dependencies installation
+pre_setup_action:
+   - action:
+        name: "com.android.tradefed.testtype.AndroidJUnitTest"
+        options:
+          - package: "android.package"
+
+dependencies:
+
+tests:
+   - test:
+       name: "com.android.tradefed.testtype.AndroidJUnitTest"
+       options:
+         - package: "android.package"
diff --git a/tests/res/testconfigs/yaml/test-config.tf_yaml b/tests/res/testconfigs/yaml/test-config.tf_yaml
index eb89137..2df8a06 100644
--- a/tests/res/testconfigs/yaml/test-config.tf_yaml
+++ b/tests/res/testconfigs/yaml/test-config.tf_yaml
@@ -1,11 +1,29 @@
 description: "Human friendly description of the test"
 
+# pre_setup_action will run before the dependencies installation
+pre_setup_action:
+   - action:
+        name: "com.android.tradefed.targetprep.RunCommandTargetPreparer"
+        options:
+          - run-command: "dumpsys value"
+   - action:
+        name: "com.android.tradefed.targetprep.RunCommandTargetPreparer"
+        options:
+          - run-command: "another one"
+
 dependencies:
    - apks: ["test.apk", "test2.apk"]
    - apks: ["test1.apk"]
    - files: ["file1.txt", "file2.txt"]
    - device_files: {"tobepushed.txt": "/sdcard", "tobepushed2.txt": "/sdcard/"}
 
+# post_setup_action will run after the dependencies installation
+post_setup_action:
+   - action:
+        name: "com.android.tradefed.targetprep.RunCommandTargetPreparer"
+        options:
+          - run-command: "dumpsys value2"
+
 tests:
    - test:
        name: "com.android.tradefed.testtype.AndroidJUnitTest"
diff --git a/tests/res/util/partial_zip.zip b/tests/res/util/partial_zip.zip
index 1f50fef..4fb046e 100644
--- a/tests/res/util/partial_zip.zip
+++ b/tests/res/util/partial_zip.zip
Binary files differ
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 832cc950..bbfbb0b 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -89,6 +89,7 @@
 import com.android.tradefed.device.cloud.GceSshTunnelMonitorTest;
 import com.android.tradefed.device.cloud.ManagedRemoteDeviceTest;
 import com.android.tradefed.device.cloud.NestedRemoteDeviceTest;
+import com.android.tradefed.device.cloud.RemoteAndroidVirtualDeviceTest;
 import com.android.tradefed.device.cloud.RemoteFileUtilTest;
 import com.android.tradefed.device.contentprovider.ContentProviderHandlerTest;
 import com.android.tradefed.device.helper.TelephonyHelperTest;
@@ -96,31 +97,22 @@
 import com.android.tradefed.device.metric.AtraceRunMetricCollectorTest;
 import com.android.tradefed.device.metric.AutoLogCollectorTest;
 import com.android.tradefed.device.metric.BaseDeviceMetricCollectorTest;
-import com.android.tradefed.device.metric.BuddyInfoMetricCollectorTest;
-import com.android.tradefed.device.metric.BugreportzMetricCollectorTest;
 import com.android.tradefed.device.metric.BugreportzOnFailureCollectorTest;
+import com.android.tradefed.device.metric.ClangCodeCoverageCollectorTest;
 import com.android.tradefed.device.metric.DebugHostLogOnFailureCollectorTest;
 import com.android.tradefed.device.metric.DeviceMetricDataTest;
-import com.android.tradefed.device.metric.DumpHeapCollectorTest;
 import com.android.tradefed.device.metric.FilePullerDeviceMetricCollectorTest;
 import com.android.tradefed.device.metric.FilePullerLogCollectorTest;
-import com.android.tradefed.device.metric.GraphicsStatsMetricCollectorTest;
+import com.android.tradefed.device.metric.GcovCodeCoverageCollectorTest;
 import com.android.tradefed.device.metric.HostStatsdMetricCollectorTest;
 import com.android.tradefed.device.metric.IncidentReportCollectorTest;
-import com.android.tradefed.device.metric.IonHeapInfoMetricCollectorTest;
+import com.android.tradefed.device.metric.JavaCodeCoverageCollectorTest;
 import com.android.tradefed.device.metric.LogcatOnFailureCollectorTest;
 import com.android.tradefed.device.metric.LogcatTimingMetricCollectorTest;
-import com.android.tradefed.device.metric.MemInfoMetricCollectorTest;
-import com.android.tradefed.device.metric.PagetypeInfoMetricCollectorTest;
 import com.android.tradefed.device.metric.PerfettoPullerMetricCollectorTest;
-import com.android.tradefed.device.metric.ProcessMaxMemoryCollectorTest;
 import com.android.tradefed.device.metric.RebootReasonCollectorTest;
 import com.android.tradefed.device.metric.RuntimeRestartCollectorTest;
-import com.android.tradefed.device.metric.ScheduleMultipleDeviceMetricCollectorTest;
-import com.android.tradefed.device.metric.ScheduledDeviceMetricCollectorTest;
 import com.android.tradefed.device.metric.ScreenshotOnFailureCollectorTest;
-import com.android.tradefed.device.metric.TemperatureCollectorTest;
-import com.android.tradefed.device.metric.TraceMetricCollectorTest;
 import com.android.tradefed.device.recovery.BatteryUnavailableDeviceRecoveryTest;
 import com.android.tradefed.device.recovery.RunConfigDeviceRecoveryTest;
 import com.android.tradefed.device.recovery.UsbResetMultiDeviceRecoveryTest;
@@ -155,6 +147,7 @@
 import com.android.tradefed.log.LogRegistryTest;
 import com.android.tradefed.log.SimpleFileLoggerTest;
 import com.android.tradefed.log.TerribleFailureEmailHandlerTest;
+import com.android.tradefed.monitoring.LabResourceDeviceMonitorTest;
 import com.android.tradefed.postprocessor.AggregatePostProcessorTest;
 import com.android.tradefed.postprocessor.AveragePostProcessorTest;
 import com.android.tradefed.postprocessor.BasePostProcessorTest;
@@ -243,6 +236,8 @@
 import com.android.tradefed.targetprep.RestartSystemServerTargetPreparerTest;
 import com.android.tradefed.targetprep.RootTargetPreparerTest;
 import com.android.tradefed.targetprep.RunCommandTargetPreparerTest;
+import com.android.tradefed.targetprep.RunOnSecondaryUserTargetPreparerTest;
+import com.android.tradefed.targetprep.RunOnWorkProfileTargetPreparerTest;
 import com.android.tradefed.targetprep.RunHostCommandTargetPreparerTest;
 import com.android.tradefed.targetprep.RunHostScriptTargetPreparerTest;
 import com.android.tradefed.targetprep.StopServicesSetupTest;
@@ -258,8 +253,8 @@
 import com.android.tradefed.targetprep.multi.MixImageZipPreparerTest;
 import com.android.tradefed.targetprep.suite.SuiteApkInstallerTest;
 import com.android.tradefed.testtype.AndroidJUnitTestTest;
+import com.android.tradefed.testtype.ArtGTestTest;
 import com.android.tradefed.testtype.ArtRunTestTest;
-import com.android.tradefed.testtype.ClangCodeCoverageListenerTest;
 import com.android.tradefed.testtype.DeviceBatteryLevelCheckerTest;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunnerTest;
 import com.android.tradefed.testtype.DeviceSuiteTest;
@@ -280,10 +275,8 @@
 import com.android.tradefed.testtype.InstrumentationSerialTestTest;
 import com.android.tradefed.testtype.InstrumentationTestTest;
 import com.android.tradefed.testtype.JarHostTestTest;
-import com.android.tradefed.testtype.JavaCodeCoverageListenerTest;
 import com.android.tradefed.testtype.NativeBenchmarkTestParserTest;
 import com.android.tradefed.testtype.NativeBenchmarkTestTest;
-import com.android.tradefed.testtype.NativeCodeCoverageListenerTest;
 import com.android.tradefed.testtype.NativeStressTestParserTest;
 import com.android.tradefed.testtype.NativeStressTestTest;
 import com.android.tradefed.testtype.NoisyDryRunTestTest;
@@ -335,6 +328,7 @@
 import com.android.tradefed.util.AaptParserTest;
 import com.android.tradefed.util.AbiFormatterTest;
 import com.android.tradefed.util.AbiUtilsTest;
+import com.android.tradefed.util.AdbRootElevatorTest;
 import com.android.tradefed.util.AppVersionFetcherTest;
 import com.android.tradefed.util.ArrayUtilTest;
 import com.android.tradefed.util.BluetoothUtilsTest;
@@ -364,7 +358,9 @@
 import com.android.tradefed.util.NativeCodeCoverageFlusherTest;
 import com.android.tradefed.util.PairTest;
 import com.android.tradefed.util.PropertyChangerTest;
+import com.android.tradefed.util.ProtoUtilTest;
 import com.android.tradefed.util.PsParserTest;
+import com.android.tradefed.util.PythonVirtualenvHelperTest;
 import com.android.tradefed.util.QuotationAwareTokenizerTest;
 import com.android.tradefed.util.RegexTrieTest;
 import com.android.tradefed.util.RemoteZipTest;
@@ -378,6 +374,7 @@
 import com.android.tradefed.util.SimpleStatsTest;
 import com.android.tradefed.util.SizeLimitedOutputStreamTest;
 import com.android.tradefed.util.Sl4aBluetoothUtilTest;
+import com.android.tradefed.util.SparseImageUtilTest;
 import com.android.tradefed.util.StreamUtilTest;
 import com.android.tradefed.util.StringEscapeUtilsTest;
 import com.android.tradefed.util.StringUtilTest;
@@ -518,7 +515,7 @@
     GceSshTunnelMonitorTest.class,
     ManagedRemoteDeviceTest.class,
     NestedRemoteDeviceTest.class,
-    RemoteAndroidDeviceTest.class,
+    RemoteAndroidVirtualDeviceTest.class,
     RemoteFileUtilTest.class,
 
     // device.contentprovider
@@ -532,31 +529,22 @@
     AtraceRunMetricCollectorTest.class,
     AutoLogCollectorTest.class,
     BaseDeviceMetricCollectorTest.class,
-    BuddyInfoMetricCollectorTest.class,
-    BugreportzMetricCollectorTest.class,
     BugreportzOnFailureCollectorTest.class,
+    ClangCodeCoverageCollectorTest.class,
     DebugHostLogOnFailureCollectorTest.class,
     DeviceMetricDataTest.class,
-    DumpHeapCollectorTest.class,
     FilePullerDeviceMetricCollectorTest.class,
     FilePullerLogCollectorTest.class,
-    GraphicsStatsMetricCollectorTest.class,
+    GcovCodeCoverageCollectorTest.class,
     IncidentReportCollectorTest.class,
-    IonHeapInfoMetricCollectorTest.class,
+    JavaCodeCoverageCollectorTest.class,
     LogcatOnFailureCollectorTest.class,
     LogcatTimingMetricCollectorTest.class,
-    MemInfoMetricCollectorTest.class,
-    PagetypeInfoMetricCollectorTest.class,
     PerfettoPullerMetricCollectorTest.class,
-    ProcessMaxMemoryCollectorTest.class,
     RebootReasonCollectorTest.class,
     RuntimeRestartCollectorTest.class,
-    ScheduledDeviceMetricCollectorTest.class,
-    ScheduleMultipleDeviceMetricCollectorTest.class,
     ScreenshotOnFailureCollectorTest.class,
     HostStatsdMetricCollectorTest.class,
-    TemperatureCollectorTest.class,
-    TraceMetricCollectorTest.class,
 
     // device.recovery
     BatteryUnavailableDeviceRecoveryTest.class,
@@ -580,7 +568,6 @@
     InvocationContextTest.class,
     InvocationExecutionTest.class,
     RemoteInvocationExecutionTest.class,
-    SandboxedInvocationExecutionTest.class,
     ShardListenerTest.class,
     ShardMainResultForwarderTest.class,
     TestInvocationMultiTest.class,
@@ -604,7 +591,6 @@
 
     // invoker.sandbox
     ParentSandboxInvocationExecutionTest.class,
-    SandboxedInvocationExecutionTest.class,
 
     // lite
     DryRunnerTest.class,
@@ -648,7 +634,6 @@
     MultiFailureDescriptionTest.class,
     SnapshotInputStreamSourceTest.class,
     SubprocessResultsReporterTest.class,
-    TestDescriptionTest.class,
     TestFailureEmailResultReporterTest.class,
     PassingTestFileReporterTest.class,
     TestDescriptionTest.class,
@@ -708,6 +693,8 @@
     RunCommandTargetPreparerTest.class,
     RunHostCommandTargetPreparerTest.class,
     RunHostScriptTargetPreparerTest.class,
+    RunOnSecondaryUserTargetPreparerTest.class,
+    RunOnWorkProfileTargetPreparerTest.class,
     StopServicesSetupTest.class,
     SystemUpdaterDeviceFlasherTest.class,
     TargetSetupErrorTest.class,
@@ -751,8 +738,8 @@
 
     // testtype
     AndroidJUnitTestTest.class,
+    ArtGTestTest.class,
     ArtRunTestTest.class,
-    ClangCodeCoverageListenerTest.class,
     CoverageMeasurementForwarderTest.class,
     DeviceBatteryLevelCheckerTest.class,
     DeviceJUnit4ClassRunnerTest.class,
@@ -774,10 +761,8 @@
     InstrumentationFileTestTest.class,
     InstrumentationTestTest.class,
     JarHostTestTest.class,
-    JavaCodeCoverageListenerTest.class,
     NativeBenchmarkTestParserTest.class,
     NativeBenchmarkTestTest.class,
-    NativeCodeCoverageListenerTest.class,
     NativeStressTestParserTest.class,
     NativeStressTestTest.class,
     NoisyDryRunTestTest.class,
@@ -848,6 +833,7 @@
     AaptParserTest.class,
     AbiFormatterTest.class,
     AbiUtilsTest.class,
+    AdbRootElevatorTest.class,
     AppVersionFetcherTest.class,
     ArrayUtilTest.class,
     BluetoothUtilsTest.class,
@@ -877,7 +863,9 @@
     MergedZipEntryCollectionTest.class,
     NativeCodeCoverageFlusherTest.class,
     PairTest.class,
+    ProtoUtilTest.class,
     PsParserTest.class,
+    PythonVirtualenvHelperTest.class,
     QuotationAwareTokenizerTest.class,
     RegexTrieTest.class,
     RemoteZipTest.class,
@@ -891,6 +879,7 @@
     SimpleStatsTest.class,
     SizeLimitedOutputStreamTest.class,
     Sl4aBluetoothUtilTest.class,
+    SparseImageUtilTest.class,
     StreamUtilTest.class,
     StringEscapeUtilsTest.class,
     StringUtilTest.class,
@@ -935,6 +924,9 @@
     // util/testmapping
     TestInfoTest.class,
     TestMappingTest.class,
+
+    // monitoring
+    LabResourceDeviceMonitorTest.class,
 })
 public class UnitTests {
     // empty of purpose
diff --git a/tests/src/com/android/tradefed/build/DeviceBuildDescriptorFuncTest.java b/tests/src/com/android/tradefed/build/DeviceBuildDescriptorFuncTest.java
index 4c180d6..f6d653d 100644
--- a/tests/src/com/android/tradefed/build/DeviceBuildDescriptorFuncTest.java
+++ b/tests/src/com/android/tradefed/build/DeviceBuildDescriptorFuncTest.java
@@ -20,8 +20,8 @@
 
 /**
  * Functional tests for {@link DeviceBuildDescriptor}.
- * <p/>
- * Sanity checks data can be retrieved off device.
+ *
+ * <p>Validity checks data can be retrieved off device.
  */
 public class DeviceBuildDescriptorFuncTest extends DeviceTestCase {
 
diff --git a/tests/src/com/android/tradefed/cluster/ClusterCommandLauncherFuncTest.java b/tests/src/com/android/tradefed/cluster/ClusterCommandLauncherFuncTest.java
index 0d3ffa1..86248bd 100644
--- a/tests/src/com/android/tradefed/cluster/ClusterCommandLauncherFuncTest.java
+++ b/tests/src/com/android/tradefed/cluster/ClusterCommandLauncherFuncTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.tradefed.cluster;
 
+import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -29,17 +30,21 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.metrics.proto.MetricMeasurement;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.util.FileUtil;
 
+import org.hamcrest.CoreMatchers;
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.InOrder;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.Spy;
 import org.mockito.junit.MockitoJUnitRunner;
 
@@ -52,13 +57,15 @@
 public class ClusterCommandLauncherFuncTest {
 
     private static final String LEGACY_TRADEFED_JAR = "/testdata/tradefed-prebuilt-cts-8.0_r21.jar";
-    private static final String LEGACY_TRADEFED_COMMAND =
-            "host --null-device --class com.android.tradefed.device.DeviceDiagTest";
+    private static final String LEGACY_TRADEFED_COMMAND = "fake.xml --null-device --run testRun PF";
+    private static final String LEGACY_TRADEFED_COMMAND_FOR_INVOCATION_FAILURE =
+            "fake.xml --null-device --fail-invocation-with-cause cause";
 
     private File mRootDir;
     private IConfiguration mConfiguration;
     private IInvocationContext mInvocationContext;
     private OptionSetter mOptionSetter;
+    @Mock private TestInformation mTestInformation;
 
     @Spy private ClusterCommandLauncher mLauncher;
     @Mock private ITestInvocationListener mListener;
@@ -81,22 +88,51 @@
         FileUtil.recursiveDelete(mRootDir);
     }
 
-    @Ignore
     @Test
     public void testRun_withLegacyTradefed()
             throws IOException, ConfigurationException, DeviceNotAvailableException {
         File tfJar = new File(mRootDir, "tradefed.jar");
         FileUtil.writeToFile(getClass().getResourceAsStream(LEGACY_TRADEFED_JAR), tfJar);
+        FileUtil.writeToFile(
+                getClass().getResourceAsStream("/config/tf/fake.xml"),
+                new File(mRootDir, "fake.xml"));
         mOptionSetter.setOptionValue("cluster:env-var", "TF_PATH", mRootDir.getAbsolutePath());
         mOptionSetter.setOptionValue("cluster:use-subprocess-reporting", "true");
         mOptionSetter.setOptionValue("cluster:command-line", LEGACY_TRADEFED_COMMAND);
 
-        mLauncher.run(mListener);
+        mLauncher.run(mTestInformation, mListener);
 
+        InOrder inOrder = Mockito.inOrder(mListener);
         HashMap<String, MetricMeasurement.Metric> emptyMap = new HashMap<>();
-        verify(mListener).testRunStarted(anyString(), anyInt(), anyInt(), anyLong());
-        verify(mListener).testStarted(any(TestDescription.class), anyLong());
-        verify(mListener).testEnded(any(TestDescription.class), anyLong(), eq(emptyMap));
-        verify(mListener).testRunEnded(anyLong(), eq(emptyMap));
+        inOrder.verify(mListener).testRunStarted(eq("testRun"), anyInt(), anyInt(), anyLong());
+        inOrder.verify(mListener).testStarted(any(TestDescription.class), anyLong());
+        inOrder.verify(mListener).testEnded(any(TestDescription.class), anyLong(), eq(emptyMap));
+        inOrder.verify(mListener).testStarted(any(TestDescription.class), anyLong());
+        inOrder.verify(mListener).testFailed(any(TestDescription.class), anyString());
+        inOrder.verify(mListener).testEnded(any(TestDescription.class), anyLong(), eq(emptyMap));
+        inOrder.verify(mListener).testRunEnded(anyLong(), eq(emptyMap));
+    }
+
+    @Test
+    public void testRun_withLegacyTradefed_invocationFailed()
+            throws IOException, ConfigurationException, DeviceNotAvailableException {
+        File tfJar = new File(mRootDir, "tradefed.jar");
+        FileUtil.writeToFile(getClass().getResourceAsStream(LEGACY_TRADEFED_JAR), tfJar);
+        FileUtil.writeToFile(
+                getClass().getResourceAsStream("/config/tf/fake.xml"),
+                new File(mRootDir, "fake.xml"));
+        mOptionSetter.setOptionValue("cluster:env-var", "TF_PATH", mRootDir.getAbsolutePath());
+        mOptionSetter.setOptionValue("cluster:use-subprocess-reporting", "true");
+        mOptionSetter.setOptionValue(
+                "cluster:command-line", LEGACY_TRADEFED_COMMAND_FOR_INVOCATION_FAILURE);
+
+        try {
+            mLauncher.run(mTestInformation, mListener);
+            fail("SubprocessCommandException should be thrown");
+        } catch (SubprocessCommandException e) {
+            Assert.assertThat(e.getCause().getMessage(), CoreMatchers.containsString("cause"));
+        }
+
+        verify(mListener).invocationFailed(any(Throwable.class));
     }
 }
diff --git a/tests/src/com/android/tradefed/cluster/ClusterCommandLauncherTest.java b/tests/src/com/android/tradefed/cluster/ClusterCommandLauncherTest.java
index c6dbe31..0dd755f 100644
--- a/tests/src/com/android/tradefed/cluster/ClusterCommandLauncherTest.java
+++ b/tests/src/com/android/tradefed/cluster/ClusterCommandLauncherTest.java
@@ -15,7 +15,6 @@
  */
 package com.android.tradefed.cluster;
 
-import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.when;
 
 import com.android.tradefed.config.Configuration;
@@ -26,6 +25,7 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.util.ArrayUtil;
 import com.android.tradefed.util.CommandResult;
@@ -54,9 +54,11 @@
 public class ClusterCommandLauncherTest {
 
     private static final String DEVICE_SERIAL = "device_serial";
+    private static final String COMMAND = "host";
 
     private IRunUtil mMockRunUtil;
     private SubprocessTestResultsParser mMockSubprocessTestResultsParser;
+    private TestInformation mMockTestInformation;
     private ITestInvocationListener mMockListener;
     private ITestDevice mMockTestDevice;
     private File mTfPath;
@@ -79,6 +81,7 @@
     public void setUp() throws Exception {
         mMockRunUtil = Mockito.mock(IRunUtil.class);
         mMockSubprocessTestResultsParser = Mockito.mock(SubprocessTestResultsParser.class);
+        mMockTestInformation = Mockito.mock(TestInformation.class);
         mMockListener = Mockito.mock(ITestInvocationListener.class);
         mMockTestDevice = Mockito.mock(ITestDevice.class);
         Mockito.doReturn(DEVICE_SERIAL).when(mMockTestDevice).getSerialNumber();
@@ -116,14 +119,15 @@
                         tfJar.getName(), mTfPath.getName(), mTfLibDir.getName());
         final List<String> jars = new ArrayList<>();
         jars.add(tfJar.getAbsolutePath());
+        jars.add(String.format("%s/", mTfPath));
         jars.add(String.format("%s/*", mTfPath));
+        jars.add(String.format("%s/", mTfLibDir));
         jars.add(String.format("%s/*", mTfLibDir));
         final String classPath = ArrayUtil.join(":", jars);
         mOptionSetter.setOptionValue("cluster:jvm-option", "-Xmx1g");
         mOptionSetter.setOptionValue("cluster:env-var", "TF_PATH", tfPathValue);
         mOptionSetter.setOptionValue("cluster:java-property", "FOO", "${TF_WORK_DIR}/foo");
-        mOptionSetter.setOptionValue("cluster:original-command-line", "original-command-line");
-        mOptionSetter.setOptionValue("cluster:command-line", "command-line");
+        mOptionSetter.setOptionValue("cluster:command-line", COMMAND);
         final String expandedTfPathValue =
                 String.format(
                         "%s:%s:%s",
@@ -139,7 +143,7 @@
                 .thenReturn(mockCommandResult);
         Mockito.when(mLauncher.getRunUtil()).thenReturn(mMockRunUtil);
 
-        mLauncher.run(mMockListener);
+        mLauncher.run(mMockTestInformation, mMockListener);
 
         Mockito.verify(mMockRunUtil, Mockito.times(2)).setWorkingDir(mRootDir);
         Mockito.verify(mMockRunUtil).unsetEnvVariable("TF_GLOBAL_CONFIG");
@@ -158,25 +162,24 @@
                                     "-Xmx1g",
                                     "-DFOO=" + mRootDir.getAbsolutePath() + "/foo",
                                     "com.android.tradefed.command.CommandRunner",
-                                    "command-line",
+                                    COMMAND,
                                     "--serial",
                                     DEVICE_SERIAL
                                 }));
-        assertTrue(new File(mRootDir, "_original-command-line.xml").exists());
     }
 
     @Test
     public void testRun_withSetupScripts()
             throws DeviceNotAvailableException, ConfigurationException {
         mInvocationContext.addAllocatedDevice("foo", mMockTestDevice);
-        final String classpath = String.format("%s/*", mTfPath);
+        final String classpath = String.format("%s/:%s/*", mTfPath, mTfPath);
         mOptionSetter.setOptionValue("cluster:env-var", "TF_PATH", mTfPath.getAbsolutePath());
         mOptionSetter.setOptionValue("cluster:env-var", "FOO", "foo");
         mOptionSetter.setOptionValue("cluster:env-var", "BAR", "bar");
         mOptionSetter.setOptionValue("cluster:env-var", "ZZZ", "zzz");
         mOptionSetter.setOptionValue("cluster:setup-script", "foo bar zzz");
         mOptionSetter.setOptionValue("cluster:setup-script", "${FOO} ${BAR} ${ZZZ}");
-        mOptionSetter.setOptionValue("cluster:command-line", "command-line");
+        mOptionSetter.setOptionValue("cluster:command-line", COMMAND);
         final CommandResult mockCommandResult = new CommandResult(CommandStatus.SUCCESS);
         when(mMockRunUtil.runTimedCmd(
                         Mockito.anyLong(),
@@ -186,7 +189,7 @@
                 .thenReturn(mockCommandResult);
         Mockito.when(mLauncher.getRunUtil()).thenReturn(mMockRunUtil);
 
-        mLauncher.run(mMockListener);
+        mLauncher.run(mMockTestInformation, mMockListener);
 
         Mockito.verify(mMockRunUtil, Mockito.times(2)).setWorkingDir(mRootDir);
         Mockito.verify(mMockRunUtil).unsetEnvVariable("TF_GLOBAL_CONFIG");
@@ -212,7 +215,7 @@
                                     "-cp",
                                     classpath,
                                     "com.android.tradefed.command.CommandRunner",
-                                    "command-line",
+                                    COMMAND,
                                     "--serial",
                                     DEVICE_SERIAL
                                 }));
@@ -222,9 +225,23 @@
     public void testRun_withUseSubprocessReporting()
             throws DeviceNotAvailableException, ConfigurationException, IOException {
         mInvocationContext.addAllocatedDevice("foo", mMockTestDevice);
-        final String classpath = String.format("%s/*", mTfPath);
-        mOptionSetter.setOptionValue("cluster:env-var", "TF_PATH", mTfPath.getAbsolutePath());
-        mOptionSetter.setOptionValue("cluster:command-line", "command-line");
+        // We need to use the real TF path to allow ClusterCommandLauncher to find a config.
+        final File tfPath =
+                new File(
+                        ClusterCommandLauncher.class
+                                .getProtectionDomain()
+                                .getCodeSource()
+                                .getLocation()
+                                .getPath());
+        String classpath;
+        if (tfPath.isDirectory()) {
+            classpath =
+                    String.format("%s/:%s/*", tfPath.getAbsolutePath(), tfPath.getAbsolutePath());
+        } else {
+            classpath = tfPath.getAbsolutePath();
+        }
+        mOptionSetter.setOptionValue("cluster:env-var", "TF_PATH", tfPath.getAbsolutePath());
+        mOptionSetter.setOptionValue("cluster:command-line", COMMAND);
         mOptionSetter.setOptionValue("cluster:use-subprocess-reporting", "true");
         when(mMockSubprocessTestResultsParser.getSocketServerPort()).thenReturn(123);
         Mockito.when(
@@ -238,18 +255,16 @@
                         Mockito.<OutputStream>any(),
                         Mockito.<String[]>any()))
                 .thenReturn(mockCommandResult);
-        final File subprocessReporterConfig = new File(mRootDir, "_command-line.xml");
         Mockito.when(mLauncher.getRunUtil()).thenReturn(mMockRunUtil);
 
-        mLauncher.run(mMockListener);
+        mLauncher.run(mMockTestInformation, mMockListener);
 
         String subprocessJar =
                 FileUtil.findFile(mRootDir, "subprocess-results-reporter.jar").getAbsolutePath();
-        assertTrue(subprocessReporterConfig.exists());
         Mockito.verify(mMockRunUtil, Mockito.times(2)).setWorkingDir(mRootDir);
         Mockito.verify(mMockRunUtil).unsetEnvVariable("TF_GLOBAL_CONFIG");
         Mockito.verify(mMockRunUtil).setEnvVariable("TF_WORK_DIR", mRootDir.getAbsolutePath());
-        Mockito.verify(mMockRunUtil).setEnvVariable("TF_PATH", mTfPath.getAbsolutePath());
+        Mockito.verify(mMockRunUtil).setEnvVariable("TF_PATH", tfPath.getAbsolutePath());
         Mockito.verify(mMockRunUtil).unsetEnvVariable("TF_GLOBAL_CONFIG");
         Mockito.verify(mMockRunUtil)
                 .runTimedCmd(
@@ -262,7 +277,7 @@
                                     "-cp",
                                     subprocessJar + ":" + classpath,
                                     "com.android.tradefed.command.CommandRunner",
-                                    subprocessReporterConfig.getName(),
+                                    COMMAND,
                                     "--serial",
                                     DEVICE_SERIAL,
                                 }));
diff --git a/tests/src/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java b/tests/src/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java
index 0a20930..adfc487 100644
--- a/tests/src/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java
+++ b/tests/src/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java
@@ -521,6 +521,23 @@
                 new String[] {CMD_LINE, "--serial", "deviceSerial"}, getExecCommandArgs());
     }
 
+    /**
+     * If a unique device serial (one with a hostname prefix) is specified for a command task,
+     * convert it to a local device serial before appending it.
+     */
+    @Test
+    public void testExecCommandWithVirtualDeviceSerial() {
+        List<ClusterCommand> cmds = new ArrayList<>();
+        ClusterCommand cmd = new ClusterCommand(COMMAND_ID, TASK_ID, CMD_LINE);
+        cmd.setTargetDeviceSerials(
+                ArrayUtil.list(ClusterHostUtil.getHostName() + ":emulator-5554"));
+        cmds.add(cmd);
+        mScheduler.execCommands(cmds);
+        assertEquals(CMD_LINE, cmds.get(0).getCommandLine());
+        assertArrayEquals(
+                new String[] {CMD_LINE, "--serial", "emulator-5554"}, getExecCommandArgs());
+    }
+
     /** Multiple serials specified for a command task. */
     @Test
     public void testExecCommandWithMultipleSerials() {
@@ -828,6 +845,70 @@
         handler.invocationInitiated(context);
     }
 
+    @Test
+    public void testInvocationEventHandler_withSubprocessCommandException() {
+        ClusterCommand mockCommand = new ClusterCommand(COMMAND_ID, TASK_ID, CMD_LINE);
+        IInvocationContext context = new InvocationContext();
+        ITestDevice mockTestDevice = EasyMock.createMock(ITestDevice.class);
+        EasyMock.expect(mockTestDevice.getSerialNumber()).andReturn(DEVICE_SERIAL);
+        EasyMock.expect(mockTestDevice.getIDevice()).andReturn(new StubDevice(DEVICE_SERIAL));
+        context.addAllocatedDevice("", mockTestDevice);
+        IBuildInfo mockBuildInfo = EasyMock.createMock(IBuildInfo.class);
+        context.addDeviceBuildInfo("", mockBuildInfo);
+        ClusterCommandScheduler.InvocationEventHandler handler =
+                mScheduler.new InvocationEventHandler(mockCommand);
+        mMockClusterOptions.setCollectEarlyTestSummary(true);
+
+        mMockEventUploader.postEvent(
+                checkClusterCommandEvent(ClusterCommandEvent.Type.InvocationInitiated));
+        mMockEventUploader.flush();
+        mMockEventUploader.postEvent(
+                checkClusterCommandEvent(ClusterCommandEvent.Type.InvocationStarted));
+        mMockEventUploader.flush();
+        mMockEventUploader.postEvent(
+                checkClusterCommandEvent(ClusterCommandEvent.Type.TestRunInProgress));
+        EasyMock.expectLastCall().anyTimes();
+        mMockEventUploader.postEvent(
+                checkClusterCommandEvent(ClusterCommandEvent.Type.InvocationEnded));
+        mMockEventUploader.flush();
+        Capture<ClusterCommandEvent> capture = new Capture<>();
+        mMockEventUploader.postEvent(EasyMock.capture(capture));
+        mMockEventUploader.flush();
+
+        EasyMock.replay(mMockEventUploader, mockBuildInfo, mockTestDevice);
+        handler.invocationInitiated(context);
+        List<TestSummary> summaries = new ArrayList<>();
+        summaries.add(
+                new TestSummary(new TestSummary.TypedString("http://uri", TestSummary.Type.URI)));
+        handler.putEarlySummary(summaries);
+        handler.putSummary(summaries);
+        handler.invocationStarted(context);
+        handler.invocationFailed(
+                new SubprocessCommandException(
+                        "error_message", new Throwable("subprocess_command_error_message")));
+        handler.invocationEnded(100L);
+        context.addAllocatedDevice(DEVICE_SERIAL, mockTestDevice);
+        Map<ITestDevice, FreeDeviceState> releaseMap = new HashMap<>();
+        releaseMap.put(mockTestDevice, FreeDeviceState.AVAILABLE);
+        handler.invocationComplete(context, releaseMap);
+        EasyMock.verify(mMockEventUploader, mockBuildInfo, mockTestDevice);
+        ClusterCommandEvent capturedEvent = capture.getValue();
+        assertTrue(capturedEvent.getType().equals(ClusterCommandEvent.Type.InvocationCompleted));
+        assertTrue(
+                ((String) capturedEvent.getData().get(ClusterCommandEvent.DATA_KEY_ERROR))
+                        .contains("SubprocessCommandException"));
+        assertEquals(
+                "subprocess_command_error_message",
+                capturedEvent.getData().get(ClusterCommandEvent.DATA_KEY_SUBPROCESS_COMMAND_ERROR));
+        assertEquals(
+                "0", capturedEvent.getData().get(ClusterCommandEvent.DATA_KEY_FAILED_TEST_COUNT));
+        assertEquals(
+                "0", capturedEvent.getData().get(ClusterCommandEvent.DATA_KEY_PASSED_TEST_COUNT));
+        assertEquals(
+                "URI: http://uri\n",
+                capturedEvent.getData().get(ClusterCommandEvent.DATA_KEY_SUMMARY));
+    }
+
     /**
      * Test that when dry-run is used we validate the config and no ConfigurationException gets
      * thrown.
@@ -1058,7 +1139,8 @@
         List<IDeviceConfiguration> deviceConfigs = config.getDeviceConfig();
         assertEquals(cmd.getTargetDeviceSerials().size(), deviceConfigs.size());
         for (int i = 0; i < cmd.getTargetDeviceSerials().size(); i++) {
-            String serial = cmd.getTargetDeviceSerials().get(i);
+            String serial =
+                    ClusterHostUtil.getLocalDeviceSerial(cmd.getTargetDeviceSerials().get(i));
             Collection<String> serials =
                     deviceConfigs.get(i).getDeviceRequirements().getSerials(null);
             assertTrue(serials.size() == 1 && serials.contains(serial));
@@ -1165,6 +1247,44 @@
         }
     }
 
+    /** Tests an execution of a managed cluster command. */
+    @Test
+    public void testExecManagedClusterCommand_virtualDeviceTest() throws Exception {
+        File workDir = null;
+        try {
+            ClusterCommand cmd = createMockManagedCommand(1);
+            cmd.setTargetDeviceSerials(
+                    ArrayUtil.list(ClusterHostUtil.getHostName() + ":emulator-5554"));
+            workDir = new File(System.getProperty("java.io.tmpdir"), cmd.getAttemptId());
+            TestEnvironment testEnvironment = createMockTestEnvironment();
+            List<TestResource> testResources = createMockTestResources();
+            TestContext testContext = new TestContext();
+            mMockClusterClient = Mockito.spy(mMockClusterClient);
+            Mockito.doReturn(testEnvironment)
+                    .when(mMockClusterClient)
+                    .getTestEnvironment(REQUEST_ID);
+            Mockito.doReturn(testResources).when(mMockClusterClient).getTestResources(REQUEST_ID);
+            Mockito.doReturn(testContext)
+                    .when(mMockClusterClient)
+                    .getTestContext(REQUEST_ID, COMMAND_ID);
+            InvocationEventHandler invocationEventHandler =
+                    mScheduler.new InvocationEventHandler(cmd);
+
+            mScheduler.execManagedClusterCommand(cmd, invocationEventHandler);
+
+            String[] args = getExecCommandArgs();
+            assertTrue(args.length > 0);
+            IConfiguration config =
+                    ConfigurationFactory.getInstance().createConfigurationFromArgs(args);
+            verifyConfig(config, cmd, testEnvironment, testResources, workDir);
+        } finally {
+            if (workDir != null) {
+                // Clean up work directory
+                FileUtil.recursiveDelete(workDir);
+            }
+        }
+    }
+
     /** Tests an execution of a managed cluster command for multiple devices. */
     @Test
     public void testExecManagedClusterCommand_multiDeviceTest() throws Exception {
@@ -1421,8 +1541,8 @@
 
         // command status is CANCELED
         mMockClusterClient = Mockito.mock(IClusterClient.class, RETURNS_DEEP_STUBS);
-        Mockito.when(mMockClusterClient.getCommandState(any(), any()))
-                .thenReturn(ClusterCommand.State.CANCELED);
+        Mockito.when(mMockClusterClient.getCommandStatus(any(), any()))
+                .thenReturn(new ClusterCommandStatus(ClusterCommand.State.CANCELED, "Reason"));
 
         // not stopped if check is disabled
         mMockClusterOptions.setCheckCommandState(false);
@@ -1434,7 +1554,7 @@
         heartbeat.run();
         assertTrue(scheduler.wasStopInvocationCalled());
 
-        Mockito.verify(mMockClusterClient, Mockito.times(1)).getCommandState(any(), any());
+        Mockito.verify(mMockClusterClient, Mockito.times(1)).getCommandStatus(any(), any());
     }
 
     /** Tests whether the heartbeat can determine the invocationId to stop. */
@@ -1450,8 +1570,8 @@
 
         // command status is CANCELED
         mMockClusterClient = Mockito.mock(IClusterClient.class, RETURNS_DEEP_STUBS);
-        Mockito.when(mMockClusterClient.getCommandState(any(), any()))
-                .thenReturn(ClusterCommand.State.CANCELED);
+        Mockito.when(mMockClusterClient.getCommandStatus(any(), any()))
+                .thenReturn(new ClusterCommandStatus(ClusterCommand.State.CANCELED, "Reason"));
 
         // not stopped without invocation context
         heartbeat.run();
@@ -1473,7 +1593,7 @@
         heartbeat.run();
         assertTrue(scheduler.wasStopInvocationCalled());
 
-        Mockito.verify(mMockClusterClient, Mockito.times(4)).getCommandState(any(), any());
+        Mockito.verify(mMockClusterClient, Mockito.times(4)).getCommandStatus(any(), any());
     }
 
     /** Tests whether the heartbeat can determine the cluster command state. */
diff --git a/tests/src/com/android/tradefed/cluster/ClusterDeviceMonitorTest.java b/tests/src/com/android/tradefed/cluster/ClusterDeviceMonitorTest.java
index fc98304..d0ba372 100644
--- a/tests/src/com/android/tradefed/cluster/ClusterDeviceMonitorTest.java
+++ b/tests/src/com/android/tradefed/cluster/ClusterDeviceMonitorTest.java
@@ -100,6 +100,24 @@
         Assert.assertEquals("cluster1", hostEvent.getClusterId());
         Assert.assertEquals(Arrays.asList("cluster2", "cluster3"), hostEvent.getNextClusterIds());
         Assert.assertEquals("lab1", hostEvent.getLabName());
+        Assert.assertEquals("", hostEvent.getData().get("label"));
+    }
+
+    @Test
+    public void testLabel() throws Exception {
+        mClusterOptionSetter.setOptionValue("cluster:label", "label1");
+        mClusterOptionSetter.setOptionValue("cluster:label", "label2");
+        Capture<ClusterHostEvent> capture = new Capture<>();
+        mHostEventUploader.postEvent(EasyMock.capture(capture));
+        mHostEventUploader.flush();
+        EasyMock.replay(mHostEventUploader);
+        mEventDispatcher.dispatch();
+        EasyMock.verify(mHostEventUploader);
+        ClusterHostEvent hostEvent = capture.getValue();
+        Assert.assertEquals("cluster1", hostEvent.getClusterId());
+        Assert.assertEquals(Arrays.asList("cluster2", "cluster3"), hostEvent.getNextClusterIds());
+        Assert.assertEquals("lab1", hostEvent.getLabName());
+        Assert.assertEquals("label1,label2", hostEvent.getData().get("label"));
     }
 
     void setOptions() throws Exception {
diff --git a/tests/src/com/android/tradefed/cluster/ClusterHostUtilTest.java b/tests/src/com/android/tradefed/cluster/ClusterHostUtilTest.java
index e2c10bd..a7ff29c 100644
--- a/tests/src/com/android/tradefed/cluster/ClusterHostUtilTest.java
+++ b/tests/src/com/android/tradefed/cluster/ClusterHostUtilTest.java
@@ -25,6 +25,8 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.easymock.EasyMock;
 import org.junit.Assert;
@@ -37,6 +39,7 @@
 public class ClusterHostUtilTest {
 
     private static final String DEVICE_SERIAL = "serial";
+    private static final String EMULATOR_SERIAL = "emulator-5554";
 
     @Test
     public void testIsIpPort() {
@@ -287,4 +290,71 @@
                         "simOperator");
         Assert.assertEquals("product", ClusterHostUtil.getRunTarget(device, format, null));
     }
+
+    @Test
+    public void testGetRunTarget_withStubDevice() {
+        final String hostname = ClusterHostUtil.getHostName();
+        // with a stub device.
+        DeviceDescriptor device =
+                new DeviceDescriptor(
+                        DEVICE_SERIAL,
+                        true,
+                        DeviceAllocationState.Available,
+                        "product",
+                        "productVariant",
+                        "sdkVersion",
+                        "buildId",
+                        "batteryLevel");
+        Assert.assertEquals(
+                hostname + ":" + DEVICE_SERIAL,
+                ClusterHostUtil.getRunTarget(device, "{SERIAL}", null));
+    }
+
+    @Test
+    public void testGetRunTarget_withEmulator() {
+        final String hostname = ClusterHostUtil.getHostName();
+        // with a stub device.
+        DeviceDescriptor device =
+                new DeviceDescriptor(
+                        EMULATOR_SERIAL,
+                        false,
+                        DeviceAllocationState.Available,
+                        "product",
+                        "productVariant",
+                        "sdkVersion",
+                        "buildId",
+                        "batteryLevel");
+        Assert.assertEquals(
+                hostname + ":" + EMULATOR_SERIAL,
+                ClusterHostUtil.getRunTarget(device, "{SERIAL}", null));
+    }
+
+    @Test
+    public void testGetRunTarget_withEmptyDeviceSerial() {
+        final String hostname = ClusterHostUtil.getHostName();
+        // with a stub device.
+        DeviceDescriptor device =
+                new DeviceDescriptor(
+                        "",
+                        false,
+                        DeviceAllocationState.Available,
+                        "product",
+                        "productVariant",
+                        "sdkVersion",
+                        "buildId",
+                        "batteryLevel");
+        Assert.assertEquals(
+                hostname + ":" + ClusterHostUtil.NULL_DEVICE_SERIAL_PLACEHOLDER,
+                ClusterHostUtil.getRunTarget(device, "{SERIAL}", null));
+    }
+
+    @Test
+    public void testGetHostIpAddress() {
+        final String hostIp = ClusterHostUtil.getHostIpAddress();
+        Assert.assertNotEquals(hostIp, "127.0.0.1");
+        Pattern pattern =
+                Pattern.compile("[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}" + "|UNKNOWN");
+        Matcher matcher = pattern.matcher(hostIp);
+        Assert.assertTrue("host ip format not match: " + hostIp, matcher.matches());
+    }
 }
diff --git a/tests/src/com/android/tradefed/cluster/SubprocessConfigBuilderTest.java b/tests/src/com/android/tradefed/cluster/SubprocessConfigBuilderTest.java
index e588940..67622f3 100644
--- a/tests/src/com/android/tradefed/cluster/SubprocessConfigBuilderTest.java
+++ b/tests/src/com/android/tradefed/cluster/SubprocessConfigBuilderTest.java
@@ -17,6 +17,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tradefed.result.LegacySubprocessResultsReporter;
 import com.android.tradefed.util.FileUtil;
@@ -43,11 +44,13 @@
     private static final String REPORTER_CLASS = LegacySubprocessResultsReporter.class.getName();
 
     private SubprocessConfigBuilder mConfigBuilder;
+    private String mClasspath;
     private File mWorkDir;
 
     @Before
     public void setUp() throws IOException {
         mConfigBuilder = new SubprocessConfigBuilder();
+        mClasspath = System.getProperty("java.class.path");
         mWorkDir = FileUtil.createTempDir("tfjar");
     }
 
@@ -58,9 +61,10 @@
 
     @Test
     public void testCreateWrapperConfig() throws Exception {
-        String oriConfigName = "testConfig";
+        String oriConfigName = "host";
         String reporterPort = "1024";
         mConfigBuilder
+                .setClasspath(mClasspath)
                 .setWorkingDir(mWorkDir)
                 .setOriginalConfig(oriConfigName)
                 .setPort(reporterPort);
@@ -69,19 +73,33 @@
         DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
         DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
         Document doc = dBuilder.parse(config);
-        verifyWrapperXml(doc, oriConfigName, reporterPort);
+        verifyWrapperXml(doc, reporterPort);
     }
 
-    private void verifyWrapperXml(Document doc, String oriConfigName, String reporterPort) {
-        NodeList inc = doc.getElementsByTagName("include");
-        assertEquals(1, inc.getLength());
-        String incName = ((Element) inc.item(0)).getAttribute("name");
-        assertEquals(oriConfigName, incName);
-        NodeList reporter = doc.getElementsByTagName("result_reporter");
-        assertEquals(1, reporter.getLength());
-        String reporterClass = ((Element) reporter.item(0)).getAttribute("class");
+    @Test
+    public void testCreateWrapperConfig_forCommandWithSlashes() throws Exception {
+        String oriConfigName = "util/timewaster";
+        String reporterPort = "1024";
+        mConfigBuilder
+                .setClasspath(mClasspath)
+                .setWorkingDir(mWorkDir)
+                .setOriginalConfig(oriConfigName)
+                .setPort(reporterPort);
+        File config = mConfigBuilder.build();
+        assertNotNull(config);
+        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
+        DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
+        Document doc = dBuilder.parse(config);
+        verifyWrapperXml(doc, reporterPort);
+    }
+
+    private void verifyWrapperXml(Document doc, String reporterPort) {
+        NodeList reporters = doc.getElementsByTagName("result_reporter");
+        assertTrue(0 < reporters.getLength());
+        Element reporter = (Element) reporters.item(reporters.getLength() - 1);
+        String reporterClass = reporter.getAttribute("class");
         assertEquals(REPORTER_CLASS, reporterClass);
-        NodeList option = ((Element) reporter.item(0)).getElementsByTagName("option");
+        NodeList option = reporter.getElementsByTagName("option");
         assertEquals(1, option.getLength());
         String optionName = ((Element) option.item(0)).getAttribute("name");
         assertEquals("subprocess-report-port", optionName);
diff --git a/tests/src/com/android/tradefed/cluster/SubprocessReportingHelperTest.java b/tests/src/com/android/tradefed/cluster/SubprocessReportingHelperTest.java
index 0dd72b0..ed2a779 100644
--- a/tests/src/com/android/tradefed/cluster/SubprocessReportingHelperTest.java
+++ b/tests/src/com/android/tradefed/cluster/SubprocessReportingHelperTest.java
@@ -34,11 +34,13 @@
 public class SubprocessReportingHelperTest {
     private SubprocessReportingHelper mHelper;
     private File mWorkDir;
+    private String mClasspath;
 
     @Before
     public void setUp() throws IOException {
-        mHelper = new SubprocessReportingHelper();
         mWorkDir = FileUtil.createTempDir("tfjar");
+        mClasspath = System.getProperty("java.class.path");
+        mHelper = new SubprocessReportingHelper("host", mClasspath, mWorkDir, null);
     }
 
     @After
@@ -48,7 +50,7 @@
 
     @Test
     public void testCreateSubprocessReporterJar() throws IOException {
-        File jar = mHelper.createSubprocessReporterJar(mWorkDir);
+        File jar = mHelper.buildSubprocessReporterJar();
         assertNotNull(jar);
         File extractedJar = ZipUtil2.extractZipToTemp(jar, "tmp-jar");
         try {
@@ -56,24 +58,9 @@
             assertNotNull(FileUtil.findFile(extractedJar, "SubprocessTestResultsParser.class"));
             assertNotNull(FileUtil.findFile(extractedJar, "SubprocessEventHelper.class"));
             assertNotNull(FileUtil.findFile(extractedJar, "SubprocessResultsReporter.class"));
+            assertNotNull(FileUtil.findFile(extractedJar, "host.xml"));
         } finally {
             FileUtil.recursiveDelete(extractedJar);
         }
     }
-
-    @Test
-    public void testGetNewCommandLine() throws IOException {
-        String oldCommandLine = "cts arg1 arg2 arg3";
-        String newCommandLine = mHelper.buildNewCommandConfig(oldCommandLine, "1024", mWorkDir);
-        assertNotNull(FileUtil.findFile(mWorkDir, "_cts.xml"));
-        assertEquals("_cts.xml arg1 arg2 arg3", newCommandLine);
-    }
-
-    @Test
-    public void testGetNewCommandLine_withSlashes() throws IOException {
-        String oldCommandLine = "foo/bar/cts arg1 arg2 arg3";
-        String newCommandLine = mHelper.buildNewCommandConfig(oldCommandLine, "1024", mWorkDir);
-        assertNotNull(FileUtil.findFile(mWorkDir, "_foo\\$bar\\$cts.xml"));
-        assertEquals("_foo$bar$cts.xml arg1 arg2 arg3", newCommandLine);
-    }
 }
diff --git a/tests/src/com/android/tradefed/command/CommandSchedulerTest.java b/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
index 793ee11..7373e33 100644
--- a/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
+++ b/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
@@ -365,12 +365,14 @@
         mScheduler =
                 new TestableCommandScheduler() {
                     @Override
-                    Map<String, ITestDevice> allocateDevices(
+                    DeviceAllocationResult allocateDevices(
                             IConfiguration config, IDeviceManager manager) {
+                        DeviceAllocationResult results = new DeviceAllocationResult();
                         Map<String, ITestDevice> allocated = new HashMap<>();
                         ((MockDeviceManager) manager).addDevice(mockDevice);
                         allocated.put("device", ((MockDeviceManager) manager).allocateDevice());
-                        return allocated;
+                        results.addAllocatedDevices(allocated);
+                        return results;
                     }
                 };
         replayMocks(mockDevice, mockListener);
@@ -1094,8 +1096,10 @@
         mMockConfiguration.validateOptions();
         replayMocks();
         mScheduler.start();
-        Map<String, ITestDevice> devices = mScheduler.allocateDevices(
-                mMockConfiguration, mMockManager);
+        DeviceAllocationResult results =
+                mScheduler.allocateDevices(mMockConfiguration, mMockManager);
+        assertTrue(results.wasAllocationSuccessful());
+        Map<String, ITestDevice> devices = results.getAllocatedDevices();
         assertEquals(1, devices.size());
         mScheduler.shutdown();
     }
@@ -1120,8 +1124,10 @@
         mMockConfiguration.setDeviceConfigList(EasyMock.anyObject());
         replayMocks();
         mScheduler.start();
-        Map<String, ITestDevice> devices =
+        DeviceAllocationResult results =
                 mScheduler.allocateDevices(mMockConfiguration, mMockManager);
+        assertTrue(results.wasAllocationSuccessful());
+        Map<String, ITestDevice> devices = results.getAllocatedDevices();
         // With replicated setup, all devices get allocated.
         assertEquals(3, devices.size());
         mScheduler.shutdown();
@@ -1147,8 +1153,10 @@
         mMockConfiguration.validateOptions();
         replayMocks();
         mScheduler.start();
-        Map<String, ITestDevice> devices = mScheduler.allocateDevices(
-                mMockConfiguration, mMockManager);
+        DeviceAllocationResult results =
+                mScheduler.allocateDevices(mMockConfiguration, mMockManager);
+        assertTrue(results.wasAllocationSuccessful());
+        Map<String, ITestDevice> devices = results.getAllocatedDevices();
         assertEquals(2, devices.size());
         assertEquals(0, mMockManager.getQueueOfAvailableDeviceSize());
         mScheduler.shutdown();
@@ -1166,8 +1174,10 @@
         mMockConfiguration.validateOptions();
         replayMocks();
         mScheduler.start();
-        Map<String, ITestDevice> devices = mScheduler.allocateDevices(
-                mMockConfiguration, mMockManager);
+        DeviceAllocationResult results =
+                mScheduler.allocateDevices(mMockConfiguration, mMockManager);
+        assertFalse(results.wasAllocationSuccessful());
+        Map<String, ITestDevice> devices = results.getAllocatedDevices();
         assertEquals(0, devices.size());
         assertEquals(2, mMockManager.getQueueOfAvailableDeviceSize());
         mScheduler.shutdown();
@@ -1279,12 +1289,14 @@
         mScheduler =
                 new TestableCommandScheduler() {
                     @Override
-                    Map<String, ITestDevice> allocateDevices(
+                    DeviceAllocationResult allocateDevices(
                             IConfiguration config, IDeviceManager manager) {
+                        DeviceAllocationResult results = new DeviceAllocationResult();
                         Map<String, ITestDevice> allocated = new HashMap<>();
                         ((MockDeviceManager) manager).addDevice(mockDevice);
                         allocated.put("device", ((MockDeviceManager) manager).allocateDevice());
-                        return allocated;
+                        results.addAllocatedDevices(allocated);
+                        return results;
                     }
                 };
 
diff --git a/tests/src/com/android/tradefed/config/ConfigurationDescriptorTest.java b/tests/src/com/android/tradefed/config/ConfigurationDescriptorTest.java
index 1a6c1b5..067fc2c 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationDescriptorTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationDescriptorTest.java
@@ -62,7 +62,7 @@
             fail("Should have thrown an exception.");
         } catch (OptionNotAllowedException expected) {
             assertEquals(
-                    "Option test-suite-tag cannot be specified via command line. "
+                    "Option 'test-suite-tag' cannot be specified via command line. "
                             + "Only in the configuration xml.",
                     expected.getMessage());
         }
diff --git a/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java b/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
index ffa3255..87e60b7 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
@@ -90,7 +90,7 @@
                 };
     }
 
-    /** Sanity test to ensure all config names on classpath are loadable */
+    /** Initial test to ensure all config names on classpath are loadable */
     public void testLoadAllConfigs() throws Exception {
         ConfigurationFactory spyFactory = Mockito.spy(mRealFactory);
         Mockito.doReturn(new HashSet<String>()).when(spyFactory).getConfigNamesFromTestCases(null);
@@ -135,9 +135,7 @@
         }
     }
 
-    /**
-     * Sanity test to ensure all configs on classpath can be fully loaded and parsed
-     */
+    /** Initial test to ensure all configs on classpath can be fully loaded and parsed */
     public void testLoadAndPrintAllConfigs() throws ConfigurationException {
         ConfigurationFactory spyFactory = Mockito.spy(mRealFactory);
         Mockito.doReturn(new HashSet<String>()).when(spyFactory).getConfigNamesFromTestCases(null);
diff --git a/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java b/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
index eded0ac..97730b4 100644
--- a/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
+++ b/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
@@ -387,7 +387,7 @@
             call.add(callableTask);
             devices.add(Mockito.mock(ITestDevice.class));
         }
-        ParallelDeviceExecutor<Set<File>> executor = new ParallelDeviceExecutor<>(devices);
+        ParallelDeviceExecutor<Set<File>> executor = new ParallelDeviceExecutor<>(devices.size());
         List<Set<File>> downloadedFile = null;
         downloadedFile = executor.invokeAll(call, 1, TimeUnit.MINUTES);
         boolean oneMustBeNonEmpty = false;
@@ -604,7 +604,7 @@
 
         IRemoteFileResolver actual = loader.load(NullFileResolver.PROTOCOL, ImmutableMap.of());
 
-        assertThat(actual).isSameAs(expected);
+        assertThat(actual).isSameInstanceAs(expected);
     }
 
     @Test
@@ -616,7 +616,7 @@
         IRemoteFileResolver resolver1 = loader1.load(NullFileResolver.PROTOCOL, ImmutableMap.of());
         IRemoteFileResolver resolver2 = loader2.load(NullFileResolver.PROTOCOL, ImmutableMap.of());
 
-        assertThat(resolver1).isNotSameAs(resolver2);
+        assertThat(resolver1).isNotSameInstanceAs(resolver2);
     }
 
     @Test
@@ -968,15 +968,9 @@
     public static final class DuplicateNullFileResolver extends NullFileResolver {}
 
     private static final Correspondence<File, File> FILE_PATH_EQUIVALENCE =
-            new Correspondence<File, File>() {
-                @Override
-                public boolean compare(File actual, File expected) {
-                    return expected.getAbsolutePath().equals(actual.getAbsolutePath());
-                }
-
-                @Override
-                public String toString() {
-                    return "is equivalent to";
-                }
-            };
+            Correspondence.from(
+                    (File actual, File expected) -> {
+                        return expected.getAbsolutePath().equals(actual.getAbsolutePath());
+                    },
+                    "is equivalent to");
 }
diff --git a/tests/src/com/android/tradefed/config/remote/GcsRemoteFileResolverTest.java b/tests/src/com/android/tradefed/config/remote/GcsRemoteFileResolverTest.java
index 0fcdaee..3f59fc0 100644
--- a/tests/src/com/android/tradefed/config/remote/GcsRemoteFileResolverTest.java
+++ b/tests/src/com/android/tradefed/config/remote/GcsRemoteFileResolverTest.java
@@ -75,9 +75,7 @@
             mResolver.resolveRemoteFiles(new File("gs:/fake/file"), new HashMap<>());
             fail("Should have thrown an exception.");
         } catch (BuildRetrievalError expected) {
-            assertEquals(
-                    "Failed to download gs:/fake/file due to: download failure",
-                    expected.getMessage());
+            assertEquals("download failure", expected.getMessage());
         }
 
         Mockito.verify(mMockHelper).fetchTestResource("gs:/fake/file");
diff --git a/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java b/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java
index 6a1c081..4784443 100644
--- a/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java
+++ b/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java
@@ -19,15 +19,18 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.android.tradefed.build.DependenciesResolver;
 import com.android.tradefed.build.StubBuildProvider;
 import com.android.tradefed.config.ConfigurationDef;
+import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.suite.SuiteResultReporter;
 import com.android.tradefed.targetprep.ITargetPreparer;
 import com.android.tradefed.targetprep.PushFilePreparer;
+import com.android.tradefed.targetprep.RunCommandTargetPreparer;
 import com.android.tradefed.targetprep.suite.SuiteApkInstaller;
 import com.android.tradefed.testtype.AndroidJUnitTest;
 
@@ -45,6 +48,8 @@
 public class ConfigurationYamlParserTest {
 
     private static final String YAML_TEST_CONFIG_1 = "/testconfigs/yaml/test-config.tf_yaml";
+    private static final String YAML_TEST_CONFIG_2 =
+            "/testconfigs/yaml/not-target-preparer.tf_yaml";
 
     private ConfigurationYamlParser mParser;
     private ConfigurationDef mConfigDef;
@@ -84,14 +89,19 @@
 
             // Dependencies
             // apk dependencies
-            assertEquals(2, config.getTargetPreparers().size());
-            ITargetPreparer installApk = config.getTargetPreparers().get(0);
+            assertEquals(5, config.getTargetPreparers().size());
+            ITargetPreparer preCommandRunner = config.getTargetPreparers().get(0);
+            assertTrue(preCommandRunner instanceof RunCommandTargetPreparer);
+            ITargetPreparer preCommandRunner2 = config.getTargetPreparers().get(1);
+            assertTrue(preCommandRunner2 instanceof RunCommandTargetPreparer);
+
+            ITargetPreparer installApk = config.getTargetPreparers().get(2);
             assertTrue(installApk instanceof SuiteApkInstaller);
             assertThat(((SuiteApkInstaller) installApk).getTestsFileName())
                     .containsExactly(
                             new File("test.apk"), new File("test2.apk"), new File("test1.apk"));
             // device file dependencies
-            ITargetPreparer pushFile = config.getTargetPreparers().get(1);
+            ITargetPreparer pushFile = config.getTargetPreparers().get(3);
             assertTrue(pushFile instanceof PushFilePreparer);
             assertThat(((PushFilePreparer) pushFile).getPushSpecs(null))
                     .containsExactly(
@@ -99,6 +109,8 @@
                             new File("tobepushed2.txt"),
                             "/sdcard",
                             new File("tobepushed.txt"));
+            ITargetPreparer postCommandRunner = config.getTargetPreparers().get(4);
+            assertTrue(postCommandRunner instanceof RunCommandTargetPreparer);
             // Result reporters
             List<ITestInvocationListener> listeners = config.getTestInvocationListeners();
             assertTrue(listeners.get(0) instanceof SuiteResultReporter);
@@ -123,14 +135,14 @@
 
             // Dependencies
             // apk dependencies
-            assertEquals(2, config.getTargetPreparers().size());
-            ITargetPreparer installApk = config.getTargetPreparers().get(0);
+            assertEquals(5, config.getTargetPreparers().size());
+            ITargetPreparer installApk = config.getTargetPreparers().get(2);
             assertTrue(installApk instanceof SuiteApkInstaller);
             assertThat(((SuiteApkInstaller) installApk).getTestsFileName())
                     .containsExactly(
                             new File("test.apk"), new File("test2.apk"), new File("test1.apk"));
             // device file dependencies
-            ITargetPreparer pushFile = config.getTargetPreparers().get(1);
+            ITargetPreparer pushFile = config.getTargetPreparers().get(3);
             assertTrue(pushFile instanceof PushFilePreparer);
             assertThat(((PushFilePreparer) pushFile).getPushSpecs(null))
                     .containsExactly(
@@ -145,6 +157,19 @@
         }
     }
 
+    @Test
+    public void testParseConfig_notTargetPreparer() throws Exception {
+        try (InputStream res = readFromRes(YAML_TEST_CONFIG_2)) {
+            mParser.parse(mConfigDef, "source", res, false);
+            try {
+                mConfigDef.createConfiguration();
+                fail("Should have thrown an exception");
+            } catch (ConfigurationException expected) {
+
+            }
+        }
+    }
+
     private InputStream readFromRes(String resourceFile) {
         return getClass().getResourceAsStream(resourceFile);
     }
diff --git a/tests/src/com/android/tradefed/device/TestDeviceTest.java b/tests/src/com/android/tradefed/device/TestDeviceTest.java
index 10ca9a5..7308581 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceTest.java
@@ -4211,11 +4211,11 @@
             Assert.assertEquals(3000, testImage.data.length);
             byte[] result = mTestDevice.compressRawImage(testImage, "PNG", true);
             // Size after compressing can vary a bit depending of the JDK
-            if (result.length != 107 && result.length != 117 && result.length != 139) {
+            if (result.length != 107 && result.length != 117) {
                 fail(
                         String.format(
                                 "Should have compress the length as expected, got %s, "
-                                        + "expected 107 or 117 or 139",
+                                        + "expected 107 or 117",
                                 result.length));
             }
 
@@ -4742,64 +4742,43 @@
     }
 
     /**
-     * Test {@link TestDevice#doesFileExist(String)} when the file exists on an sdcard from another
-     * user.
+     * Test {@link TestDevice#doesFileExist(String)} using content provider when the file is in
+     * external storage path.
      */
     public void testDoesFileExists_sdcard() throws Exception {
-        mTestDevice =
-                new TestableTestDevice() {
-                    @Override
-                    public int getCurrentUser()
-                            throws DeviceNotAvailableException, DeviceRuntimeException {
-                        return 10;
-                    }
-                };
-        injectShellResponse("ls \"/storage/emulated/10/file\"", "file");
+        mTestDevice = createTestDevice();
+
+        TestableTestDevice spy = (TestableTestDevice) Mockito.spy(mTestDevice);
+        ContentProviderHandler cp = Mockito.mock(ContentProviderHandler.class);
+        doReturn(cp).when(spy).getContentProvider();
+
+        final String fakeFile = "/sdcard/file";
+        final String targetFilePath = "/storage/emulated/10/file";
+
+        doReturn("").when(spy).executeShellCommand(Mockito.contains("content query --user 10"));
+
         EasyMock.replay(mMockIDevice);
-        assertTrue(mTestDevice.doesFileExist("/sdcard/file"));
+        spy.doesFileExist(fakeFile);
         EasyMock.verify(mMockIDevice);
+
+        verify(spy, times(1)).getContentProvider();
+        verify(cp, times(1)).doesFileExist(targetFilePath);
     }
 
     /** Push a file using the content provider. */
     public void testPushFile_contentProvider() throws Exception {
-        mTestDevice =
-                new TestableTestDevice() {
-                    @Override
-                    public int getApiLevel() throws DeviceNotAvailableException {
-                        return 29;
-                    }
-
-                    @Override
-                    public int getCurrentUser()
-                            throws DeviceNotAvailableException, DeviceRuntimeException {
-                        return 10;
-                    }
-
-                    @Override
-                    public boolean isPackageInstalled(String packageName, String userId)
-                            throws DeviceNotAvailableException {
-                        return false;
-                    }
-                };
+        mTestDevice = createTestDevice();
         TestableTestDevice spy = (TestableTestDevice) Mockito.spy(mTestDevice);
+        setupContentProvider(spy);
+
         final String fakeRemotePath = "/sdcard/";
         File tmpFile = FileUtil.createTempFile("push", ".test");
-        doReturn(null)
-                .when(spy)
-                .installPackage(Mockito.any(), Mockito.anyBoolean(), Mockito.anyBoolean());
-        CommandResult setLegacy = new CommandResult(CommandStatus.SUCCESS);
-        doReturn(setLegacy).when(spy).executeShellV2Command(Mockito.contains("cmd appops set"));
-
-        CommandResult getLegacy = new CommandResult(CommandStatus.SUCCESS);
-        getLegacy.setStdout("LEGACY_STORAGE: allow");
-        doReturn(getLegacy).when(spy).executeShellV2Command(Mockito.contains("cmd appops get"));
 
         CommandResult writeContent = new CommandResult(CommandStatus.SUCCESS);
         writeContent.setStdout("");
         doReturn(writeContent)
                 .when(spy)
                 .executeShellV2Command(Mockito.contains("content write"), (File) Mockito.any());
-        doReturn(null).when(spy).uninstallPackage(Mockito.eq("android.tradefed.contentprovider"));
         EasyMock.replay(mMockIDevice);
         try {
             boolean res = spy.pushFile(tmpFile, fakeRemotePath);
@@ -4820,37 +4799,12 @@
 
     /** Push a file using the content provider. */
     public void testPushFile_contentProvider_notFound() throws Exception {
-        mTestDevice =
-                new TestableTestDevice() {
-                    @Override
-                    public int getApiLevel() throws DeviceNotAvailableException {
-                        return 29;
-                    }
-
-                    @Override
-                    public int getCurrentUser()
-                            throws DeviceNotAvailableException, DeviceRuntimeException {
-                        return 10;
-                    }
-
-                    @Override
-                    public boolean isPackageInstalled(String packageName, String userId)
-                            throws DeviceNotAvailableException {
-                        return false;
-                    }
-                };
+        mTestDevice = createTestDevice();
         TestableTestDevice spy = (TestableTestDevice) Mockito.spy(mTestDevice);
+        setupContentProvider(spy);
+
         final String fakeRemotePath = "/sdcard/";
         File tmpFile = FileUtil.createTempFile("push", ".test");
-        doReturn(null)
-                .when(spy)
-                .installPackage(Mockito.any(), Mockito.anyBoolean(), Mockito.anyBoolean());
-        CommandResult setLegacy = new CommandResult(CommandStatus.SUCCESS);
-        doReturn(setLegacy).when(spy).executeShellV2Command(Mockito.contains("cmd appops set"));
-
-        CommandResult getLegacy = new CommandResult(CommandStatus.SUCCESS);
-        getLegacy.setStdout("LEGACY_STORAGE: allow");
-        doReturn(getLegacy).when(spy).executeShellV2Command(Mockito.contains("cmd appops get"));
 
         CommandResult writeContent = new CommandResult(CommandStatus.SUCCESS);
         writeContent.setStdout("");
@@ -4896,4 +4850,38 @@
                                 EasyMock.eq(property)))
                 .andReturn(stubResult);
     }
+
+    private void setupContentProvider(TestableTestDevice spy) throws Exception {
+        doReturn(null)
+                .when(spy)
+                .installPackage(Mockito.any(), Mockito.anyBoolean(), Mockito.anyBoolean());
+        CommandResult setLegacy = new CommandResult(CommandStatus.SUCCESS);
+        doReturn(setLegacy).when(spy).executeShellV2Command(Mockito.contains("cmd appops set"));
+
+        CommandResult getLegacy = new CommandResult(CommandStatus.SUCCESS);
+        getLegacy.setStdout("LEGACY_STORAGE: allow");
+        doReturn(getLegacy).when(spy).executeShellV2Command(Mockito.contains("cmd appops get"));
+
+        doReturn(null).when(spy).uninstallPackage(Mockito.eq("android.tradefed.contentprovider"));
+    }
+
+    private TestableTestDevice createTestDevice() {
+        return new TestableTestDevice() {
+            @Override
+            public int getApiLevel() throws DeviceNotAvailableException {
+                return 29;
+            }
+
+            @Override
+            public int getCurrentUser() throws DeviceNotAvailableException, DeviceRuntimeException {
+                return 10;
+            }
+
+            @Override
+            public boolean isPackageInstalled(String packageName, String userId)
+                    throws DeviceNotAvailableException {
+                return false;
+            }
+        };
+    }
 }
diff --git a/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java b/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
index e8d0219..18109d1 100644
--- a/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
@@ -58,6 +58,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -323,6 +324,32 @@
     /** Test {@link RemoteAndroidVirtualDevice#postInvocationTearDown(Throwable)}. */
     @Test
     public void testPostInvocationTearDown() throws Exception {
+        mTestDevice =
+                new TestableRemoteAndroidVirtualDevice() {
+                    @Override
+                    protected IRunUtil getRunUtil() {
+                        return mMockRunUtil;
+                    }
+
+                    @Override
+                    void createGceSshMonitor(
+                            ITestDevice device,
+                            IBuildInfo buildInfo,
+                            HostAndPort hostAndPort,
+                            TestDeviceOptions deviceOptions) {
+                        // ignore
+                    }
+
+                    @Override
+                    GceManager getGceHandler() {
+                        return mGceHandler;
+                    }
+
+                    @Override
+                    public DeviceDescriptor getDeviceDescriptor() {
+                        return null;
+                    }
+                };
         mTestDevice.setTestLogger(mTestLogger);
         EasyMock.expect(mMockStateMonitor.waitForDeviceNotAvailable(EasyMock.anyLong()))
                 .andReturn(true);
@@ -803,4 +830,83 @@
         }
         verifyMocks();
     }
+
+    /**
+     * Run powerwash() but GceAvdInfo = null, RemoteAndroidVirtualDevice choose to throw exception.
+     */
+    @Test
+    public void testPowerwashNoAvdInfo() throws Exception {
+        final String expectedException = "Can not get GCE AVD Info. launch GCE first? [ : ]";
+        EasyMock.replay(mMockRunUtil, mMockIDevice);
+        try {
+            mTestDevice.powerwashGce();
+            fail("Should have thrown an exception");
+        } catch (TargetSetupError expected) {
+            assertEquals(expectedException, expected.getMessage());
+        }
+        EasyMock.verify(mMockRunUtil, mMockIDevice);
+    }
+
+    /** Test powerwash GCE command */
+    @Test
+    public void testPowerwashGce() throws Exception {
+        mTestDevice =
+                new TestableRemoteAndroidVirtualDevice() {
+                    @Override
+                    public IDevice getIDevice() {
+                        return mMockIDevice;
+                    }
+
+                    @Override
+                    GceManager getGceHandler() {
+                        return mGceHandler;
+                    }
+
+                    @Override
+                    void createGceSshMonitor(
+                            ITestDevice device,
+                            IBuildInfo buildInfo,
+                            HostAndPort hostAndPort,
+                            TestDeviceOptions deviceOptions) {
+                        // ignore
+                    }
+                };
+        String instanceUser = "user1";
+        IBuildInfo mMockBuildInfo = EasyMock.createMock(IBuildInfo.class);
+        OptionSetter setter = new OptionSetter(mTestDevice.getOptions());
+        setter.setOptionValue("instance-user", instanceUser);
+        String powerwashCommand = String.format("/home/%s/bin/powerwash_cvd", instanceUser);
+        String avdConnectHost = String.format("%s@127.0.0.1", instanceUser);
+        GceAvdInfo gceAvd =
+                new GceAvdInfo(
+                        instanceUser, HostAndPort.fromHost("127.0.0.1"), null, GceStatus.SUCCESS);
+        doReturn(gceAvd).when(mGceHandler).startGce(null);
+        OutputStream stdout = null;
+        OutputStream stderr = null;
+        CommandResult powerwashCmdResult = new CommandResult(CommandStatus.SUCCESS);
+        EasyMock.expect(
+                        mMockRunUtil.runTimedCmd(
+                                EasyMock.anyLong(),
+                                EasyMock.eq(stdout),
+                                EasyMock.eq(stderr),
+                                EasyMock.eq("ssh"),
+                                EasyMock.eq("-o"),
+                                EasyMock.eq("UserKnownHostsFile=/dev/null"),
+                                EasyMock.eq("-o"),
+                                EasyMock.eq("StrictHostKeyChecking=no"),
+                                EasyMock.eq("-o"),
+                                EasyMock.eq("ServerAliveInterval=10"),
+                                EasyMock.eq("-i"),
+                                EasyMock.anyObject(),
+                                EasyMock.eq(avdConnectHost),
+                                EasyMock.eq(powerwashCommand)))
+                .andReturn(powerwashCmdResult);
+        EasyMock.expect(mMockStateMonitor.waitForDeviceAvailable(EasyMock.anyLong()))
+                .andReturn(mMockIDevice);
+        EasyMock.replay(mMockRunUtil, mMockIDevice);
+        // Launch GCE before powerwash.
+        mTestDevice.launchGce(mMockBuildInfo);
+        mTestDevice.powerwashGce();
+        EasyMock.verify(mMockRunUtil, mMockIDevice);
+    }
 }
diff --git a/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
index b0da7d4..0570a59 100644
--- a/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
+++ b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.util.CommandResult;
@@ -369,6 +370,36 @@
                 espacedUrl);
     }
 
+    /** Test {@link ContentProviderHandler#doesFileExist(String)}. */
+    @Test
+    public void testDoesFileExist() throws Exception {
+        String devicePath = "path/somewhere/file.txt";
+
+        when(mMockDevice.getCurrentUser()).thenReturn(99);
+        when(mMockDevice.executeShellCommand(
+                        "content query --user 99 --uri "
+                                + ContentProviderHandler.createEscapedContentUri(devicePath)))
+                .thenReturn("");
+
+        assertTrue(mProvider.doesFileExist(devicePath));
+    }
+
+    /**
+     * Test {@link ContentProviderHandler#doesFileExist(String)} returns false when 'adb shell
+     * content query' returns no results.
+     */
+    @Test
+    public void testDoesFileExist_NotExists() throws Exception {
+        String devicePath = "path/somewhere/";
+
+        when(mMockDevice.getCurrentUser()).thenReturn(99);
+        when(mMockDevice.executeShellCommand(
+                        "content query --user 99 --uri "
+                                + ContentProviderHandler.createEscapedContentUri(devicePath)))
+                .thenReturn("No result found.\n");
+        assertFalse(mProvider.doesFileExist(devicePath));
+    }
+
     @Test
     public void testParseQueryResultRow() {
         String row =
diff --git a/tests/src/com/android/tradefed/device/metric/BuddyInfoMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/BuddyInfoMetricCollectorTest.java
deleted file mode 100644
index 27bf498..0000000
--- a/tests/src/com/android/tradefed/device/metric/BuddyInfoMetricCollectorTest.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verify;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.io.File;
-
-/** Unit tests for {@link BuddyInfoMetricCollector}. */
-// TODO(b/71868090): Consolidate all the individual metric collector tests into one common tests.
-@RunWith(JUnit4.class)
-public class BuddyInfoMetricCollectorTest {
-    @Mock IInvocationContext mContext;
-
-    @Mock ITestInvocationListener mListener;
-
-    @Mock ITestDevice device;
-
-    @Spy BuddyInfoMetricCollector mBuddyInfoMetricCollector;
-
-    @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mBuddyInfoMetricCollector.init(mContext, mListener);
-
-        doNothing()
-                .when(mListener)
-                .testLog(anyString(), eq(LogDataType.TEXT), any(InputStreamSource.class));
-
-        doReturn(new File("unusable-index-1"))
-                .when(mBuddyInfoMetricCollector)
-                .saveProcessOutput(any(ITestDevice.class), anyString(), anyString());
-
-        doReturn(tempFolder.newFolder()).when(mBuddyInfoMetricCollector).createTempDir();
-    }
-
-    @Test
-    public void testCollect() throws Exception {
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-
-        mBuddyInfoMetricCollector.collect(device, runData);
-
-        // Verify that we logged the metric file.
-        verify(mListener).testLog(eq("unusable-index-1"), eq(LogDataType.TEXT), any());
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/BugreportzMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/BugreportzMetricCollectorTest.java
deleted file mode 100644
index 6b19eb4..0000000
--- a/tests/src/com/android/tradefed/device/metric/BugreportzMetricCollectorTest.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.result.ITestInvocationListener;
-import java.util.Arrays;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-/** Unit tests for {@link BugreportzMetricCollector}. */
-@RunWith(JUnit4.class)
-public class BugreportzMetricCollectorTest {
-    @Mock IInvocationContext mContext;
-
-    @Mock ITestDevice mTestDevice;
-
-    @Mock ITestInvocationListener mForwarder;
-
-    @Spy BugreportzMetricCollector mBugreportzMetricCollector;
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        doReturn(Arrays.asList(mTestDevice)).when(mContext).getDevices();
-
-        when(mTestDevice.logBugreport(anyString(), any(ITestInvocationListener.class)))
-                .thenReturn(true);
-
-        mBugreportzMetricCollector.init(mContext, mForwarder);
-
-        when(mBugreportzMetricCollector.getFileSuffix()).thenReturn("1");
-    }
-
-    /** Tests successful collection of bugreport. */
-    @Test
-    public void testCollect_success() throws Exception {
-        when(mTestDevice.logBugreport("bugreportz-1", mForwarder)).thenReturn(true);
-
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-
-        mBugreportzMetricCollector.collect(mTestDevice, runData);
-
-        verify(mTestDevice).logBugreport(eq("bugreport-1"), eq(mForwarder));
-    }
-}
diff --git a/tests/src/com/android/tradefed/testtype/ClangCodeCoverageListenerTest.java b/tests/src/com/android/tradefed/device/metric/ClangCodeCoverageCollectorTest.java
similarity index 84%
rename from tests/src/com/android/tradefed/testtype/ClangCodeCoverageListenerTest.java
rename to tests/src/com/android/tradefed/device/metric/ClangCodeCoverageCollectorTest.java
index 69f60fd..c354f03 100644
--- a/tests/src/com/android/tradefed/testtype/ClangCodeCoverageListenerTest.java
+++ b/tests/src/com/android/tradefed/device/metric/ClangCodeCoverageCollectorTest.java
@@ -14,20 +14,23 @@
  * limitations under the License.
  */
 
-package com.android.tradefed.testtype;
+package com.android.tradefed.device.metric;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.build.IBuildProvider;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.metrics.proto.MetricMeasurement;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.InputStreamSource;
@@ -38,7 +41,6 @@
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
-import com.google.common.base.VerifyException;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.protobuf.ByteString;
@@ -51,7 +53,9 @@
 import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.mockito.InOrder;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
 
@@ -70,7 +74,7 @@
 
 /** Unit tests for {@link ClangCodeCoverageListener}. */
 @RunWith(JUnit4.class)
-public class ClangCodeCoverageListenerTest {
+public class ClangCodeCoverageCollectorTest {
 
     private static final String RUN_NAME = "SomeTest";
     private static final int TEST_COUNT = 5;
@@ -86,6 +90,7 @@
     @Mock IConfiguration mMockConfiguration;
     @Mock IBuildProvider mMockBuildProvider;
     @Mock ITestDevice mMockDevice;
+    @Mock IInvocationContext mMockContext;
     @Spy CommandArgumentCaptor mCommandArgumentCaptor;
     LogFileReader mFakeListener = new LogFileReader();
 
@@ -95,7 +100,7 @@
     OptionSetter mCoverageOptionsSetter = null;
 
     /** Object under test. */
-    ClangCodeCoverageListener mListener;
+    ClangCodeCoverageCollector mListener;
 
     @Before
     public void setUp() throws Exception {
@@ -111,13 +116,17 @@
         doReturn(mMockBuildProvider).when(mMockConfiguration).getBuildProvider();
         doReturn(mMockBuildInfo).when(mMockBuildProvider).getBuild();
 
-        mListener = new ClangCodeCoverageListener(mMockDevice, mFakeListener);
+        doReturn(ImmutableList.of(mMockDevice)).when(mMockContext).getDevices();
+
+        mListener = new ClangCodeCoverageCollector();
         mListener.setConfiguration(mMockConfiguration);
         mListener.setRunUtil(mCommandArgumentCaptor);
     }
 
     @Test
     public void coverageDisabled_noCoverageLog() {
+        mListener.init(mMockContext, mFakeListener);
+
         // Simulate a test run.
         mListener.testRunStarted(RUN_NAME, TEST_COUNT);
         mListener.testRunEnded(ELAPSED_TIME, mMetrics);
@@ -131,6 +140,8 @@
     public void clangCoverageDisabled_noCoverageLog() throws Exception {
         mCoverageOptionsSetter.setOptionValue("coverage", "true");
 
+        mListener.init(mMockContext, mFakeListener);
+
         // Simulate a test run.
         mListener.testRunStarted(RUN_NAME, TEST_COUNT);
         mListener.testRunEnded(ELAPSED_TIME, mMetrics);
@@ -147,17 +158,18 @@
         mCoverageOptionsSetter.setOptionValue("coverage-flush", "true");
 
         // Setup mocks.
-        doReturn(true).when(mMockDevice).enableAdbRoot();
         doReturn(true).when(mMockDevice).isAdbRoot();
         doReturn(createTar(ImmutableMap.of())).when(mMockDevice).pullFile(anyString());
 
         // Simulate a test run.
+        mListener.init(mMockContext, mFakeListener);
         mListener.testRunStarted(RUN_NAME, TEST_COUNT);
         mListener.testRunEnded(ELAPSED_TIME, mMetrics);
         mListener.invocationEnded(ELAPSED_TIME);
 
-        // Verify the flush-all-coverage command was called.
-        verify(mMockDevice).executeShellCommand("kill -37 -1");
+        // Verify the flush-all-coverage command was called twice - once on init() and once during
+        // the end of the test run.
+        verify(mMockDevice, times(2)).executeShellCommand("kill -37 -1");
     }
 
     @Test
@@ -166,7 +178,7 @@
         mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "CLANG");
 
         // Setup mocks.
-        doReturn(true).when(mMockDevice).enableAdbRoot();
+        doReturn(true).when(mMockDevice).isAdbRoot();
         File tarGz =
                 createTar(
                         ImmutableMap.of(
@@ -179,6 +191,7 @@
         doReturn(createProfileToolZip()).when(mMockBuildInfo).getFile(anyString());
 
         // Simulate a test run.
+        mListener.init(mMockContext, mFakeListener);
         mListener.testRunStarted(RUN_NAME, TEST_COUNT);
         mListener.testRunEnded(ELAPSED_TIME, mMetrics);
         mListener.invocationEnded(ELAPSED_TIME);
@@ -203,7 +216,7 @@
         mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "CLANG");
 
         // Setup mocks.
-        doReturn(true).when(mMockDevice).enableAdbRoot();
+        doReturn(true).when(mMockDevice).isAdbRoot();
         File tarGz =
                 createTar(
                         ImmutableMap.of(
@@ -216,6 +229,7 @@
         doReturn(createProfileToolZip()).when(mMockBuildInfo).getFile(anyString());
 
         // Simulate a test run.
+        mListener.init(mMockContext, mFakeListener);
         mListener.testRunStarted(RUN_NAME, TEST_COUNT);
         mListener.testRunEnded(ELAPSED_TIME, mMetrics);
         mListener.invocationEnded(ELAPSED_TIME);
@@ -237,10 +251,11 @@
         mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "CLANG");
 
         // Setup mocks.
-        doReturn(true).when(mMockDevice).enableAdbRoot();
+        doReturn(true).when(mMockDevice).isAdbRoot();
         doReturn(createTar(ImmutableMap.of())).when(mMockDevice).pullFile(anyString());
 
         // Simulate a test run.
+        mListener.init(mMockContext, mFakeListener);
         mListener.testRunStarted(RUN_NAME, TEST_COUNT);
         mListener.testRunEnded(ELAPSED_TIME, mMetrics);
         mListener.invocationEnded(ELAPSED_TIME);
@@ -256,7 +271,7 @@
         mCoverageOptionsSetter.setOptionValue("llvm-profdata-path", "/path/to/some/directory");
 
         // Setup mocks.
-        doReturn(true).when(mMockDevice).enableAdbRoot();
+        doReturn(true).when(mMockDevice).isAdbRoot();
         File tarGz =
                 createTar(
                         ImmutableMap.of(
@@ -267,6 +282,7 @@
         doReturn(tarGz).when(mMockDevice).pullFile(anyString());
 
         // Simulate a test run.
+        mListener.init(mMockContext, mFakeListener);
         mListener.testRunStarted(RUN_NAME, TEST_COUNT);
         mListener.testRunEnded(ELAPSED_TIME, mMetrics);
         mListener.invocationEnded(ELAPSED_TIME);
@@ -277,12 +293,12 @@
     }
 
     @Test
-    public void testProfileToolNotFound_throwsException() throws Exception {
+    public void testProfileToolNotFound_noLog() throws Exception {
         mCoverageOptionsSetter.setOptionValue("coverage", "true");
         mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "CLANG");
 
         // Setup mocks.
-        doReturn(true).when(mMockDevice).enableAdbRoot();
+        doReturn(true).when(mMockDevice).isAdbRoot();
         File tarGz =
                 createTar(
                         ImmutableMap.of(
@@ -293,27 +309,22 @@
         doReturn(tarGz).when(mMockDevice).pullFile(anyString());
 
         // Simulate a test run.
-        try {
-            mListener.testRunStarted(RUN_NAME, TEST_COUNT);
-            mListener.testRunEnded(ELAPSED_TIME, mMetrics);
-            mListener.invocationEnded(ELAPSED_TIME);
-            fail("an exception should have been thrown");
-        } catch (VerifyException e) {
-            // Expected.
-            assertThat(e).hasMessageThat().contains("llvm-profdata");
-        }
+        mListener.init(mMockContext, mFakeListener);
+        mListener.testRunStarted(RUN_NAME, TEST_COUNT);
+        mListener.testRunEnded(ELAPSED_TIME, mMetrics);
+        mListener.invocationEnded(ELAPSED_TIME);
 
         // Verify testLog(..) was never called.
         assertThat(mFakeListener.getLogs()).isEmpty();
     }
 
     @Test
-    public void testProfileToolFailed_throwsException() throws Exception {
+    public void testProfileToolFailed_noLog() throws Exception {
         mCoverageOptionsSetter.setOptionValue("coverage", "true");
         mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "CLANG");
 
         // Setup mocks.
-        doReturn(true).when(mMockDevice).enableAdbRoot();
+        doReturn(true).when(mMockDevice).isAdbRoot();
         File tarGz =
                 createTar(
                         ImmutableMap.of(
@@ -327,20 +338,36 @@
         mCommandArgumentCaptor.setResult(CommandStatus.FAILED);
 
         // Simulate a test run.
-        try {
-            mListener.testRunStarted(RUN_NAME, TEST_COUNT);
-            mListener.testRunEnded(ELAPSED_TIME, mMetrics);
-            fail("an exception should have been thrown");
-        } catch (RuntimeException e) {
-            // Expected.
-            assertThat(e).hasMessageThat().contains("merge Clang profile data");
-        }
+        mListener.init(mMockContext, mFakeListener);
+        mListener.testRunStarted(RUN_NAME, TEST_COUNT);
+        mListener.testRunEnded(ELAPSED_TIME, mMetrics);
         mListener.invocationEnded(ELAPSED_TIME);
 
         // Verify testLog(..) was never called.
         assertThat(mFakeListener.getLogs()).isEmpty();
     }
 
+    @Test
+    public void testInit_adbRootAndCoverageFlush() throws Exception {
+        mCoverageOptionsSetter.setOptionValue("coverage", "true");
+        mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "CLANG");
+
+        // Setup mocks.
+        when(mMockDevice.isAdbRoot()).thenReturn(false).thenReturn(true);
+        doReturn(true).when(mMockDevice).enableAdbRoot();
+
+        // Call init(...).
+        mListener.init(mMockContext, mFakeListener);
+
+        // Verify.
+        InOrder inOrder = Mockito.inOrder(mMockDevice);
+        inOrder.verify(mMockDevice).isAdbRoot();
+        inOrder.verify(mMockDevice).enableAdbRoot();
+        inOrder.verify(mMockDevice).executeShellCommand("kill -37 -1");
+        inOrder.verify(mMockDevice).executeShellCommand(anyString());
+        inOrder.verify(mMockDevice).disableAdbRoot();
+    }
+
     abstract static class CommandArgumentCaptor implements IRunUtil {
         private List<String> mCommand;
         private CommandResult mResult = new CommandResult(CommandStatus.SUCCESS);
diff --git a/tests/src/com/android/tradefed/device/metric/DumpHeapCollectorTest.java b/tests/src/com/android/tradefed/device/metric/DumpHeapCollectorTest.java
deleted file mode 100644
index 6a9cb56..0000000
--- a/tests/src/com/android/tradefed/device/metric/DumpHeapCollectorTest.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.util.FileUtil;
-import com.google.common.truth.Truth;
-import java.io.File;
-import java.util.Arrays;
-import java.util.List;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-/** Unit tests for {@link DumpHeapCollector}. */
-@RunWith(JUnit4.class)
-public class DumpHeapCollectorTest {
-
-    @Mock private IInvocationContext mContext;
-
-    @Mock private ITestInvocationListener mListener;
-
-    @Mock private ITestDevice mDevice;
-
-    @Rule public TemporaryFolder folder = new TemporaryFolder();
-
-    @Spy private DumpHeapCollector mDumpheapCollector;
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        when(mDevice.executeShellCommand("dumpsys meminfo -c | grep camera"))
-                .thenReturn("proc,native,camera,21348,800,N/A,e\n");
-
-        when(mDevice.executeShellCommand("dumpsys meminfo -c | grep maps"))
-                .thenReturn("proc,native,maps,21349,900,N/A,e\n");
-
-        when(mDevice.executeShellCommand("am dumpheap camera /data/local/tmp/camera_trigger.hprof"))
-                .thenReturn("");
-
-        doNothing()
-                .when(mListener)
-                .testLog(anyString(), any(LogDataType.class), any(InputStreamSource.class));
-    }
-
-    @Test
-    public void testTakeDumpheap_success() throws Exception {
-        File mapsDumpheap1 = folder.newFile("maps1");
-        File mapsDumpheap2 = folder.newFile("maps2");
-
-        doReturn("1").when(mDumpheapCollector).getFileSuffix();
-
-        when(mDevice.dumpHeap("maps", "/data/local/tmp/maps_trigger_1.hprof"))
-                .thenReturn(mapsDumpheap1)
-                .thenReturn(mapsDumpheap2);
-
-        String fakeDumpheapOutput =
-                "proc,native,maps,21349,900,N/A,e\nproc,native,camera,21350,800,N/A,e\n";
-
-        List<File> files =
-                mDumpheapCollector.takeDumpheap(mDevice, fakeDumpheapOutput, "maps", 850L);
-
-        Truth.assertThat(files).containsExactly(mapsDumpheap1);
-    }
-
-    @Test
-    public void testCollect_success() throws Exception {
-        File tempFile1 = folder.newFile();
-        File tempFile2 = folder.newFile();
-        when(mDevice.dumpHeap(anyString(), anyString()))
-                .thenReturn(tempFile1)
-                .thenReturn(tempFile2);
-
-        OptionSetter options = new OptionSetter(mDumpheapCollector);
-
-        options.setOptionValue("dumpheap-thresholds", "camera", "700");
-        options.setOptionValue("dumpheap-thresholds", "maps", "800");
-
-        mDumpheapCollector.init(mContext, mListener);
-
-        mDumpheapCollector.collect(mDevice, null);
-
-        ArgumentCaptor<String> dataNameCaptor = ArgumentCaptor.forClass(String.class);
-        ArgumentCaptor<LogDataType> dataTypeCaptor = ArgumentCaptor.forClass(LogDataType.class);
-        ArgumentCaptor<InputStreamSource> inputCaptor =
-                ArgumentCaptor.forClass(InputStreamSource.class);
-
-        verify(mListener, times(2))
-                .testLog(dataNameCaptor.capture(), dataTypeCaptor.capture(), inputCaptor.capture());
-
-        // Assert that the correct filename was sent to testLog.
-        Truth.assertThat(dataNameCaptor.getAllValues())
-                .containsExactlyElementsIn(
-                        Arrays.asList(
-                                FileUtil.getBaseName(tempFile1.getName()),
-                                FileUtil.getBaseName(tempFile2.getName())));
-
-        // Assert that the correct data type was sent to testLog.
-        Truth.assertThat(dataTypeCaptor.getAllValues())
-                .containsExactlyElementsIn(Arrays.asList(LogDataType.HPROF, LogDataType.HPROF));
-    }
-
-    @Test
-    public void testCollectSuccess_thresholdTooHigh() throws Exception {
-        File tempFile1 = folder.newFile();
-        File tempFile2 = folder.newFile();
-        when(mDevice.pullFile(anyString())).thenReturn(tempFile1).thenReturn(tempFile2);
-
-        OptionSetter options = new OptionSetter(mDumpheapCollector);
-
-        options.setOptionValue("dumpheap-thresholds", "camera", "7000");
-        options.setOptionValue("dumpheap-thresholds", "maps", "8000");
-
-        mDumpheapCollector.init(mContext, mListener);
-
-        mDumpheapCollector.collect(mDevice, null);
-
-        ArgumentCaptor<String> dataNameCaptor = ArgumentCaptor.forClass(String.class);
-        ArgumentCaptor<LogDataType> dataTypeCaptor = ArgumentCaptor.forClass(LogDataType.class);
-        ArgumentCaptor<InputStreamSource> inputCaptor =
-                ArgumentCaptor.forClass(InputStreamSource.class);
-
-        verify(mListener, times(0))
-                .testLog(dataNameCaptor.capture(), dataTypeCaptor.capture(), inputCaptor.capture());
-    }
-
-    @Test
-    public void testCollectNoError_processNotFound() throws Exception {
-        // Make the meminfo dump not contain the heap info of fake_process.
-        when(mDevice.executeShellCommand("dumpsys meminfo -c | grep fake_process")).thenReturn("");
-
-        OptionSetter options = new OptionSetter(mDumpheapCollector);
-        options.setOptionValue("dumpheap-thresholds", "fake_process", "7000");
-
-        mDumpheapCollector.init(mContext, mListener);
-
-        mDumpheapCollector.collect(mDevice, null);
-
-        ArgumentCaptor<String> dataNameCaptor = ArgumentCaptor.forClass(String.class);
-        ArgumentCaptor<LogDataType> dataTypeCaptor = ArgumentCaptor.forClass(LogDataType.class);
-        ArgumentCaptor<InputStreamSource> inputCaptor =
-                ArgumentCaptor.forClass(InputStreamSource.class);
-
-        // Verify that no testLog calls were made.
-        verify(mListener, times(0))
-                .testLog(dataNameCaptor.capture(), dataTypeCaptor.capture(), inputCaptor.capture());
-    }
-}
-
diff --git a/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java b/tests/src/com/android/tradefed/device/metric/GcovCodeCoverageCollectorTest.java
similarity index 75%
rename from tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java
rename to tests/src/com/android/tradefed/device/metric/GcovCodeCoverageCollectorTest.java
index 6d7e90e..6ce6c58 100644
--- a/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java
+++ b/tests/src/com/android/tradefed/device/metric/GcovCodeCoverageCollectorTest.java
@@ -14,26 +14,28 @@
  * limitations under the License.
  */
 
-package com.android.tradefed.testtype;
+package com.android.tradefed.device.metric;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.testtype.coverage.CoverageOptions;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
-import com.google.common.base.VerifyException;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.protobuf.ByteString;
 
@@ -45,7 +47,9 @@
 import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.mockito.InOrder;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.io.File;
@@ -64,9 +68,9 @@
 import java.util.Map;
 import java.util.zip.ZipFile;
 
-/** Unit tests for {@link NativeCodeCoverageListener}. */
+/** Unit tests for {@link GcovCodeCoverageCollector}. */
 @RunWith(JUnit4.class)
-public class NativeCodeCoverageListenerTest {
+public class GcovCodeCoverageCollectorTest {
 
     private static final String RUN_NAME = "SomeTest";
     private static final int TEST_COUNT = 5;
@@ -74,6 +78,8 @@
 
     @Rule public TemporaryFolder folder = new TemporaryFolder();
 
+    @Mock IConfiguration mMockConfiguration;
+    @Mock IInvocationContext mMockContext;
     @Mock ITestDevice mMockDevice;
 
     LogFileReader mFakeListener = new LogFileReader();
@@ -84,7 +90,7 @@
     OptionSetter mCoverageOptionsSetter = null;
 
     /** Object under test. */
-    NativeCodeCoverageListener mCodeCoverageListener;
+    GcovCodeCoverageCollector mCodeCoverageListener;
 
     @Before
     public void setUp() throws ConfigurationException {
@@ -92,14 +98,19 @@
 
         mCoverageOptions = new CoverageOptions();
         mCoverageOptionsSetter = new OptionSetter(mCoverageOptions);
+
+        doReturn(mCoverageOptions).when(mMockConfiguration).getCoverageOptions();
+        doReturn(ImmutableList.of(mMockDevice)).when(mMockContext).getDevices();
+
+        mCodeCoverageListener = new GcovCodeCoverageCollector();
+        mCodeCoverageListener.setConfiguration(mMockConfiguration);
     }
 
     @Test
-    public void test_logsCoverageZip() throws DeviceNotAvailableException, IOException {
-        mCodeCoverageListener = new NativeCodeCoverageListener(mMockDevice, mFakeListener);
-
+    public void test_logsCoverageZip() throws Exception {
+        enableGcovCoverage();
         // Setup mocks to write the coverage measurement to the file.
-        doReturn(true).when(mMockDevice).enableAdbRoot();
+        doReturn(true).when(mMockDevice).isAdbRoot();
         File tar =
                 createTar(
                         ImmutableMap.of(
@@ -110,6 +121,7 @@
         doReturn(tar).when(mMockDevice).pullFile(anyString());
 
         // Simulate a test run.
+        mCodeCoverageListener.init(mMockContext, mFakeListener);
         mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
         Map<String, String> metric = new HashMap<>();
         mCodeCoverageListener.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
@@ -135,13 +147,13 @@
     }
 
     @Test
-    public void testNoCoverageFiles_logsEmptyZip() throws DeviceNotAvailableException, IOException {
-        mCodeCoverageListener = new NativeCodeCoverageListener(mMockDevice, mFakeListener);
-
-        doReturn(true).when(mMockDevice).enableAdbRoot();
+    public void testNoCoverageFiles_logsEmptyZip() throws Exception {
+        enableGcovCoverage();
+        doReturn(true).when(mMockDevice).isAdbRoot();
         doReturn(createTar(ImmutableMap.of())).when(mMockDevice).pullFile(anyString());
 
         // Simulate a test run.
+        mCodeCoverageListener.init(mMockContext, mFakeListener);
         mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
         Map<String, String> metric = new HashMap<>();
         mCodeCoverageListener.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
@@ -160,69 +172,60 @@
     }
 
     @Test
-    public void testCoverageFlushAllProcesses_flushAllCommandCalled()
-            throws ConfigurationException, DeviceNotAvailableException, IOException {
+    public void testCoverageFlushAllProcesses_flushAllCommandCalled() throws Exception {
+        enableGcovCoverage();
         mCoverageOptionsSetter.setOptionValue("coverage-flush", "true");
 
-        mCodeCoverageListener =
-                new NativeCodeCoverageListener(mMockDevice, mCoverageOptions, mFakeListener);
-
-        doReturn(true).when(mMockDevice).enableAdbRoot();
         doReturn(true).when(mMockDevice).isAdbRoot();
         doReturn(createTar(ImmutableMap.of())).when(mMockDevice).pullFile(anyString());
 
         // Simulate a test run.
+        mCodeCoverageListener.init(mMockContext, mFakeListener);
         mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
         Map<String, String> metric = new HashMap<>();
         mCodeCoverageListener.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
 
-        // Verify the flush-all-coverage command was called.
-        verify(mMockDevice).executeShellCommand("kill -37 -1");
+        // Verify the flush-all-coverage command was called twice - once on init(...) and once
+        // on test run end.
+        verify(mMockDevice, times(2)).executeShellCommand("kill -37 -1");
     }
 
     @Test
-    public void testCoverageFlushSpecificProcesses_flushCommandCalled()
-            throws ConfigurationException, DeviceNotAvailableException, IOException {
+    public void testCoverageFlushSpecificProcesses_flushCommandCalled() throws Exception {
+        enableGcovCoverage();
         mCoverageOptionsSetter.setOptionValue("coverage-flush", "true");
         mCoverageOptionsSetter.setOptionValue("coverage-processes", "mediaserver");
         mCoverageOptionsSetter.setOptionValue("coverage-processes", "adbd");
 
-        mCodeCoverageListener =
-                new NativeCodeCoverageListener(mMockDevice, mCoverageOptions, mFakeListener);
-
-        doReturn(true).when(mMockDevice).enableAdbRoot();
         doReturn(true).when(mMockDevice).isAdbRoot();
         doReturn("123").when(mMockDevice).getProcessPid("mediaserver");
         doReturn("56789").when(mMockDevice).getProcessPid("adbd");
         doReturn(createTar(ImmutableMap.of())).when(mMockDevice).pullFile(anyString());
 
         // Simulate a test run.
+        mCodeCoverageListener.init(mMockContext, mFakeListener);
         mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
         Map<String, String> metric = new HashMap<>();
         mCodeCoverageListener.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
 
-        // Verify the flush-coverage command was called with the specific pids.
-        verify(mMockDevice).executeShellCommand("kill -37 123 56789");
+        // Verify the flush-coverage command was called with the specific pids twice - once on
+        // init(...) and once on test run end.
+        verify(mMockDevice, times(2)).executeShellCommand("kill -37 123 56789");
     }
 
     @Test
-    public void testFailure_unableToPullFile() throws DeviceNotAvailableException {
-        mCodeCoverageListener = new NativeCodeCoverageListener(mMockDevice, mFakeListener);
-
+    public void testFailure_unableToPullFile() throws Exception {
+        enableGcovCoverage();
         // Setup mocks.
-        doReturn(true).when(mMockDevice).enableAdbRoot();
+        doReturn(true).when(mMockDevice).isAdbRoot();
 
         // Simulate a test run.
+        mCodeCoverageListener.init(mMockContext, mFakeListener);
         mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
 
         Map<String, String> metric = new HashMap<>();
-        try {
             mCodeCoverageListener.testRunEnded(
                     ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
-            fail("an exception should have been thrown.");
-        } catch (VerifyException e) {
-            // Expected
-        }
 
         // Verify testLog(..) was not called.
         assertThat(mFakeListener.getLogs()).isEmpty();
@@ -230,10 +233,14 @@
 
     @Test
     public void testNoCollectOnTestEnd_noCoverageMeasurements() throws Exception {
-        mCodeCoverageListener = new NativeCodeCoverageListener(mMockDevice, mFakeListener);
+        enableGcovCoverage();
         mCodeCoverageListener.setCollectOnTestEnd(false);
 
-        // Simute a test run.
+        // Setup mocks.
+        doReturn(true).when(mMockDevice).isAdbRoot();
+
+        // Simulate a test run.
+        mCodeCoverageListener.init(mMockContext, mFakeListener);
         mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
         Map<String, String> metric = new HashMap<>();
         mCodeCoverageListener.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
@@ -268,6 +275,25 @@
         }
     }
 
+    @Test
+    public void testInit_adbRootAndCoverageFlushed() throws Exception {
+        enableGcovCoverage();
+
+        // Setup mocks.
+        when(mMockDevice.isAdbRoot()).thenReturn(false).thenReturn(true);
+        when(mMockDevice.enableAdbRoot()).thenReturn(true);
+
+        // Test init(...).
+        mCodeCoverageListener.init(mMockContext, mFakeListener);
+
+        InOrder inOrder = Mockito.inOrder(mMockDevice);
+        inOrder.verify(mMockDevice).isAdbRoot();
+        inOrder.verify(mMockDevice).enableAdbRoot();
+        inOrder.verify(mMockDevice).executeShellCommand("kill -37 -1");
+        inOrder.verify(mMockDevice).executeShellCommand(anyString());
+        inOrder.verify(mMockDevice).disableAdbRoot();
+    }
+
     /** An {@link ITestInvocationListener} which reads test log data streams for verification. */
     private static class LogFileReader implements ITestInvocationListener {
         private List<ByteString> mLogs = new ArrayList<>();
@@ -303,4 +329,9 @@
         }
         return tarFile;
     }
+
+    private void enableGcovCoverage() throws ConfigurationException {
+        mCoverageOptionsSetter.setOptionValue("coverage", "true");
+        mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "GCOV");
+    }
 }
diff --git a/tests/src/com/android/tradefed/device/metric/GraphicsStatsMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/GraphicsStatsMetricCollectorTest.java
deleted file mode 100644
index cb051a6..0000000
--- a/tests/src/com/android/tradefed/device/metric/GraphicsStatsMetricCollectorTest.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verify;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.io.File;
-
-/** Unit tests for {@link GraphicsStatsMetricCollector}. */
-//TODO(b/71868090): Consolidate all the individual metric collector tests into one common tests.
-@RunWith(JUnit4.class)
-public class GraphicsStatsMetricCollectorTest {
-    @Mock IInvocationContext mContext;
-
-    @Mock ITestInvocationListener mListener;
-
-    @Mock ITestDevice mDevice;
-
-    @Spy GraphicsStatsMetricCollector mGfxInfoMetricCollector;
-
-    @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mGfxInfoMetricCollector.init(mContext, mListener);
-
-        doNothing()
-                .when(mListener)
-                .testLog(anyString(), eq(LogDataType.GFX_INFO), any(InputStreamSource.class));
-
-        doReturn(new File("graphics-1"))
-                .when(mGfxInfoMetricCollector)
-                .saveProcessOutput(any(ITestDevice.class), anyString(), anyString());
-
-        doReturn(tempFolder.newFolder()).when(mGfxInfoMetricCollector).createTempDir();
-    }
-
-    @Test
-    public void testCollect() throws Exception {
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-
-        mGfxInfoMetricCollector.collect(mDevice, runData);
-
-        // Verify that we logged the metric file.
-        verify(mListener).testLog(eq("graphics-1"), eq(LogDataType.GFX_INFO), any());
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/IncidentReportCollectorTest.java b/tests/src/com/android/tradefed/device/metric/IncidentReportCollectorTest.java
index b5c142d..cce1aac 100644
--- a/tests/src/com/android/tradefed/device/metric/IncidentReportCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/IncidentReportCollectorTest.java
@@ -27,9 +27,11 @@
 import android.os.IncidentProto;
 
 import com.android.tradefed.config.ConfigurationDef;
+import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ByteArrayInputStreamSource;
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.InputStreamSource;
@@ -37,6 +39,8 @@
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
 
 import org.junit.After;
@@ -49,6 +53,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.StandardOpenOption;
@@ -62,6 +67,7 @@
 public class IncidentReportCollectorTest {
     private static final TestDescription FAKE_TEST = new TestDescription("class", "test");
     private static final String FAKE_REPORT_PATH = "/sdcard/incidents/final.pb";
+    private static final long FAKE_TIME = 10000;
 
     // The {@code IncidentReportCollector} under test.
     private IncidentReportCollector mIncidentCollector;
@@ -153,7 +159,7 @@
         // Replay the test started and test ended events to simulate a test.
         mIncidentCollector.testStarted(FAKE_TEST);
         mIncidentCollector.testEnded(FAKE_TEST, TfMetricProtoUtil.upgradeConvert(deviceMetrics));
-        // Ensure the file was processed and contains the expected report.
+        // Ensure the metrics/files were not pulled or processed.
         verify(mInvocationListener, never()).testLog(any(), any(), any());
         verify(mMockTestDevice, never()).pullFile(any(String.class));
     }
@@ -176,4 +182,32 @@
         // Ensure the processed file was not successfully processed or reported.
         verify(mInvocationListener, never()).testLog(matches(".*processed.*"), any(), any());
     }
+
+    /** Tests that a report is collected and reported if the test run end option is set. */
+    @Test
+    public void testCollectOnTestRunEnd() throws Exception {
+        OptionSetter setter = new OptionSetter(mIncidentCollector);
+        setter.setOptionValue("incident-on-test-run-end", "true");
+        // Replay the test started and test ended events to simulate a test.
+        mIncidentCollector.testStarted(FAKE_TEST);
+        mIncidentCollector.testEnded(
+                FAKE_TEST, TfMetricProtoUtil.upgradeConvert(new HashMap<String, String>()));
+        // Call test run end and ensure something is collected.
+        when(mMockTestDevice.executeShellV2Command(
+                        eq(IncidentReportCollector.INCIDENT_REPORT_CMD),
+                        any(FileOutputStream.class)))
+                .thenReturn(new CommandResult(CommandStatus.SUCCESS));
+        mIncidentCollector.testRunEnded(FAKE_TIME, new HashMap<String, Metric>());
+        // Ensure the expected "on-test-run-end" files were logged and processed.
+        verify(mInvocationListener)
+                .testLog(
+                        matches(".*incident-on-test-run-end.*"),
+                        eq(LogDataType.PB),
+                        any(FileInputStreamSource.class));
+        verify(mInvocationListener)
+                .testLog(
+                        matches(".*incident-on-test-run-end.*-processed"),
+                        eq(LogDataType.PB),
+                        any(ByteArrayInputStreamSource.class));
+    }
 }
diff --git a/tests/src/com/android/tradefed/device/metric/IonHeapInfoMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/IonHeapInfoMetricCollectorTest.java
deleted file mode 100644
index 2547672..0000000
--- a/tests/src/com/android/tradefed/device/metric/IonHeapInfoMetricCollectorTest.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device.metric;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verify;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.io.File;
-
-/** Unit tests for {@link IonHeapInfoMetricCollector}. */
-// TODO(b/71868090): Consolidate all the individual metric collector tests into one common tests.
-@RunWith(JUnit4.class)
-public class IonHeapInfoMetricCollectorTest {
-    @Mock IInvocationContext mContext;
-
-    @Mock ITestInvocationListener mListener;
-
-    @Mock ITestDevice mDevice;
-
-    @Spy IonHeapInfoMetricCollector mIonHeapInfoMetricCollector;
-
-    @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mIonHeapInfoMetricCollector.init(mContext, mListener);
-
-        doNothing()
-                .when(mListener)
-                .testLog(anyString(), eq(LogDataType.TEXT), any(InputStreamSource.class));
-
-        doReturn(new File("ion-system-2"))
-                .when(mIonHeapInfoMetricCollector)
-                .saveProcessOutput(
-                        any(ITestDevice.class), eq("cat /d/ion/heaps/system"), anyString());
-        doReturn(new File("ion-audio-1"))
-                .when(mIonHeapInfoMetricCollector)
-                .saveProcessOutput(
-                        any(ITestDevice.class), eq("cat /d/ion/heaps/audio"), anyString());
-
-        doReturn(tempFolder.newFolder()).when(mIonHeapInfoMetricCollector).createTempDir();
-    }
-
-    @Test
-    public void testCollect() throws Exception {
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-
-        mIonHeapInfoMetricCollector.collect(mDevice, runData);
-
-        // Verify that we logged the metric file.
-        verify(mListener).testLog(eq("ion-system-2"), eq(LogDataType.TEXT), any());
-        verify(mListener).testLog(eq("ion-audio-1"), eq(LogDataType.TEXT), any());
-    }
-}
diff --git a/tests/src/com/android/tradefed/testtype/JavaCodeCoverageListenerTest.java b/tests/src/com/android/tradefed/device/metric/JavaCodeCoverageCollectorTest.java
similarity index 62%
rename from tests/src/com/android/tradefed/testtype/JavaCodeCoverageListenerTest.java
rename to tests/src/com/android/tradefed/device/metric/JavaCodeCoverageCollectorTest.java
index b074de5..d4a42a3 100644
--- a/tests/src/com/android/tradefed/testtype/JavaCodeCoverageListenerTest.java
+++ b/tests/src/com/android/tradefed/device/metric/JavaCodeCoverageCollectorTest.java
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.tradefed.testtype;
+package com.android.tradefed.device.metric;
 
-import static com.android.tradefed.testtype.JavaCodeCoverageListener.MERGE_COVERAGE_MEASUREMENTS_TEST_NAME;
+import static com.android.tradefed.device.metric.JavaCodeCoverageCollector.MERGE_COVERAGE_MEASUREMENTS_TEST_NAME;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -30,9 +30,12 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.InputStreamSource;
@@ -41,7 +44,6 @@
 import com.android.tradefed.util.JavaCodeCoverageFlusher;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
-import com.google.common.base.VerifyException;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.protobuf.ByteString;
@@ -74,9 +76,9 @@
 import java.util.List;
 import java.util.Map;
 
-/** Unit tests for {@link JavaCodeCoverageListener}. */
+/** Unit tests for {@link JavaCodeCoverageCollector}. */
 @RunWith(JUnit4.class)
-public class JavaCodeCoverageListenerTest {
+public class JavaCodeCoverageCollectorTest {
 
     private static final int PROBE_COUNT = 10;
 
@@ -90,13 +92,15 @@
 
     @Rule public TemporaryFolder folder = new TemporaryFolder();
 
+    @Mock IConfiguration mMockConfiguration;
+    @Mock IInvocationContext mMockContext;
     @Mock ITestDevice mMockDevice;
     @Mock JavaCodeCoverageFlusher mMockFlusher;
 
     @Spy LogFileReader mFakeListener = new LogFileReader();
 
     /** Object under test. */
-    JavaCodeCoverageListener mCodeCoverageListener;
+    JavaCodeCoverageCollector mCodeCoverageCollector;
 
     CoverageOptions mCoverageOptions = null;
     OptionSetter mCoverageOptionsSetter = null;
@@ -108,17 +112,38 @@
         mCoverageOptions = new CoverageOptions();
         mCoverageOptionsSetter = new OptionSetter(mCoverageOptions);
 
+        when(mMockConfiguration.getCoverageOptions()).thenReturn(mCoverageOptions);
+
+        when(mMockContext.getDevices()).thenReturn(ImmutableList.of(mMockDevice));
+
         // Mock an unrooted device that has no issues enabling or disabling root.
         when(mMockDevice.isAdbRoot()).thenReturn(false);
         when(mMockDevice.enableAdbRoot()).thenReturn(true);
         when(mMockDevice.disableAdbRoot()).thenReturn(true);
 
-        mCodeCoverageListener =
-                new JavaCodeCoverageListener(mMockDevice, mCoverageOptions, false, mFakeListener);
+        mCodeCoverageCollector = new JavaCodeCoverageCollector();
+        mCodeCoverageCollector.setConfiguration(mMockConfiguration);
+    }
+
+    @Test
+    public void testRunEnded_noCoverageEnabled_noop() throws Exception {
+        // Setup mocks.
+        HashMap<String, Metric> runMetrics = new HashMap<>();
+
+        // Simulate a test run.
+        mCodeCoverageCollector.init(mMockContext, mFakeListener);
+        mCodeCoverageCollector.testRunStarted(RUN_NAME, TEST_COUNT);
+        mCodeCoverageCollector.testRunEnded(ELAPSED_TIME, runMetrics);
+
+        // Verify testLog(..) was not called.
+        verify(mFakeListener, never())
+                .testLog(anyString(), eq(LogDataType.COVERAGE), eq(COVERAGE_MEASUREMENT));
     }
 
     @Test
     public void testRunEnded_rootEnabled_logsCoverageMeasurement() throws Exception {
+        enableJavaCoverage();
+
         // Setup mocks.
         HashMap<String, Metric> runMetrics = createMetricsWithCoverageMeasurement(DEVICE_PATH);
         mockCoverageFileOnDevice(DEVICE_PATH);
@@ -126,8 +151,9 @@
         doReturn("").when(mMockDevice).executeShellCommand(anyString());
 
         // Simulate a test run.
-        mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
-        mCodeCoverageListener.testRunEnded(ELAPSED_TIME, runMetrics);
+        mCodeCoverageCollector.init(mMockContext, mFakeListener);
+        mCodeCoverageCollector.testRunStarted(RUN_NAME, TEST_COUNT);
+        mCodeCoverageCollector.testRunEnded(ELAPSED_TIME, runMetrics);
 
         // Verify testLog(..) was called with the coverage file.
         verify(mFakeListener)
@@ -138,10 +164,17 @@
     }
 
     @Test
-    public void testFailure_noCoverageMetric() {
+    public void testFailure_noCoverageMetric() throws Exception {
+        enableJavaCoverage();
+
+        // Setup mocks.
+        when(mMockDevice.executeShellCommand("ps -e")).thenReturn("");
+        when(mMockDevice.executeShellCommand("pm list packages -a")).thenReturn("");
+
         // Simulate a test run.
-        mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
-        mCodeCoverageListener.testRunEnded(ELAPSED_TIME, new HashMap<String, Metric>());
+        mCodeCoverageCollector.init(mMockContext, mFakeListener);
+        mCodeCoverageCollector.testRunStarted(RUN_NAME, TEST_COUNT);
+        mCodeCoverageCollector.testRunEnded(ELAPSED_TIME, new HashMap<String, Metric>());
 
         // Verify that the test run is marked as a failure.
         verify(mFakeListener).testRunFailed(anyString());
@@ -152,33 +185,33 @@
     }
 
     @Test
-    public void testFailure_unableToPullFile() throws DeviceNotAvailableException {
+    public void testFailure_unableToPullFile() throws Exception {
+        enableJavaCoverage();
         HashMap<String, Metric> runMetrics = createMetricsWithCoverageMeasurement(DEVICE_PATH);
         doReturn("").when(mMockDevice).executeShellCommand(anyString());
         doReturn(null).when(mMockDevice).pullFile(DEVICE_PATH);
 
         // Simulate a test run.
-        mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
-        try {
-            mCodeCoverageListener.testRunEnded(ELAPSED_TIME, runMetrics);
-            fail("Exception not thrown");
-        } catch (VerifyException expected) {
-        }
+        mCodeCoverageCollector.init(mMockContext, mFakeListener);
+        mCodeCoverageCollector.testRunStarted(RUN_NAME, TEST_COUNT);
+        mCodeCoverageCollector.testRunEnded(ELAPSED_TIME, runMetrics);
 
-        // Verify testLog(..) was not called.
         verify(mFakeListener, never())
                 .testLog(anyString(), eq(LogDataType.COVERAGE), any(InputStreamSource.class));
     }
 
     @Test
     public void testRunEnded_rootDisabled_enablesRootBeforePullingFiles() throws Exception {
+        enableJavaCoverage();
         HashMap<String, Metric> runMetrics = createMetricsWithCoverageMeasurement(DEVICE_PATH);
         mockCoverageFileOnDevice(DEVICE_PATH);
         when(mMockDevice.isAdbRoot()).thenReturn(false);
         doReturn("").when(mMockDevice).executeShellCommand(anyString());
 
-        mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
-        mCodeCoverageListener.testRunEnded(ELAPSED_TIME, runMetrics);
+        // Simulate a test run.
+        mCodeCoverageCollector.init(mMockContext, mFakeListener);
+        mCodeCoverageCollector.testRunStarted(RUN_NAME, TEST_COUNT);
+        mCodeCoverageCollector.testRunEnded(ELAPSED_TIME, runMetrics);
 
         InOrder inOrder = inOrder(mMockDevice);
         inOrder.verify(mMockDevice).enableAdbRoot();
@@ -187,29 +220,37 @@
     }
 
     @Test
-    public void testRunEnded_rootDisabled_throwsIfCannotEnableRoot() throws Exception {
+    public void testRunEnded_rootDisabled_noLogIfCannotEnableRoot() throws Exception {
+        enableJavaCoverage();
         HashMap<String, Metric> runMetrics = createMetricsWithCoverageMeasurement(DEVICE_PATH);
         mockCoverageFileOnDevice(DEVICE_PATH);
         when(mMockDevice.isAdbRoot()).thenReturn(false);
         when(mMockDevice.enableAdbRoot()).thenReturn(false);
 
-        mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
+        // Simulate a test run.
         try {
-            mCodeCoverageListener.testRunEnded(ELAPSED_TIME, runMetrics);
-            fail("Exception not thrown");
-        } catch (RuntimeException expected) {
+            mCodeCoverageCollector.init(mMockContext, mFakeListener);
+            fail("An exception should have been thrown.");
+        } catch (RuntimeException e) {
+            // Expected.
         }
+
+        verify(mFakeListener, never())
+                .testLog(anyString(), eq(LogDataType.COVERAGE), any(InputStreamSource.class));
     }
 
     @Test
     public void testRunEnded_rootDisabled_disablesRootAfterPullingFiles() throws Exception {
+        enableJavaCoverage();
         HashMap<String, Metric> runMetrics = createMetricsWithCoverageMeasurement(DEVICE_PATH);
         mockCoverageFileOnDevice(DEVICE_PATH);
         when(mMockDevice.isAdbRoot()).thenReturn(false);
         doReturn("").when(mMockDevice).executeShellCommand(anyString());
 
-        mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
-        mCodeCoverageListener.testRunEnded(ELAPSED_TIME, runMetrics);
+        // Simulate a test run.
+        mCodeCoverageCollector.init(mMockContext, mFakeListener);
+        mCodeCoverageCollector.testRunStarted(RUN_NAME, TEST_COUNT);
+        mCodeCoverageCollector.testRunEnded(ELAPSED_TIME, runMetrics);
 
         InOrder inOrder = inOrder(mMockDevice);
         inOrder.verify(mMockDevice).pullFile(anyString());
@@ -218,27 +259,13 @@
     }
 
     @Test
-    public void testRunEnded_rootDisabled_throwsIfCannotDisableRoot() throws Exception {
-        HashMap<String, Metric> runMetrics = createMetricsWithCoverageMeasurement(DEVICE_PATH);
-        mockCoverageFileOnDevice(DEVICE_PATH);
-        when(mMockDevice.isAdbRoot()).thenReturn(false);
-        when(mMockDevice.disableAdbRoot()).thenReturn(false);
+    public void testMerge_producesSingleMeasurement() throws Exception {
+        enableJavaCoverage();
 
-        mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
-        try {
-            mCodeCoverageListener.testRunEnded(ELAPSED_TIME, runMetrics);
-            fail("Exception not thrown");
-        } catch (RuntimeException expected) {
-        }
-    }
-
-    @Test
-    public void testMerge_producesSingleMeasurement()
-            throws DeviceNotAvailableException, IOException {
         // Setup mocks.
         File coverageFile1 = folder.newFile("coverage1.ec");
         try (OutputStream out = new FileOutputStream(coverageFile1)) {
-            ByteString measurement = measurement(fullyCovered(JavaCodeCoverageListener.class));
+            ByteString measurement = measurement(fullyCovered(JavaCodeCoverageCollector.class));
             measurement.writeTo(out);
         }
 
@@ -246,13 +273,12 @@
         try (OutputStream out = new FileOutputStream(coverageFile2)) {
             ByteString measurement =
                     measurement(
-                            partiallyCovered(JavaCodeCoverageListener.class),
-                            partiallyCovered(JavaCodeCoverageListenerTest.class));
+                            partiallyCovered(JavaCodeCoverageCollector.class),
+                            partiallyCovered(JavaCodeCoverageCollectorTest.class));
             measurement.writeTo(out);
         }
 
-        mCodeCoverageListener =
-                new JavaCodeCoverageListener(mMockDevice, mCoverageOptions, true, mFakeListener);
+        mCodeCoverageCollector.setMergeMeasurements(true);
 
         Map<String, String> metric = new HashMap<>();
         metric.put("coverageFilePath", DEVICE_PATH);
@@ -261,12 +287,13 @@
         doReturn("").when(mMockDevice).executeShellCommand(anyString());
         doReturn(coverageFile1).doReturn(coverageFile2).when(mMockDevice).pullFile(DEVICE_PATH);
 
-        mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
-        mCodeCoverageListener.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
-        mCodeCoverageListener.testRunStarted(RUN_NAME + "2", TEST_COUNT);
-        mCodeCoverageListener.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
-        mCodeCoverageListener.testRunStarted(MERGE_COVERAGE_MEASUREMENTS_TEST_NAME, TEST_COUNT);
-        mCodeCoverageListener.testRunEnded(ELAPSED_TIME, new HashMap<String, Metric>());
+        mCodeCoverageCollector.init(mMockContext, mFakeListener);
+        mCodeCoverageCollector.testRunStarted(RUN_NAME, TEST_COUNT);
+        mCodeCoverageCollector.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
+        mCodeCoverageCollector.testRunStarted(RUN_NAME + "2", TEST_COUNT);
+        mCodeCoverageCollector.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
+        mCodeCoverageCollector.testRunStarted(MERGE_COVERAGE_MEASUREMENTS_TEST_NAME, TEST_COUNT);
+        mCodeCoverageCollector.testRunEnded(ELAPSED_TIME, new HashMap<String, Metric>());
 
         // Capture the merged coverage measurements that were passed to the fake listener.
         ArgumentCaptor<ByteString> stream = ArgumentCaptor.forClass(ByteString.class);
@@ -280,18 +307,20 @@
         boolean[] fullyCovered = new boolean[PROBE_COUNT];
         Arrays.fill(fullyCovered, Boolean.TRUE);
 
-        assertThat(execData.contains(vmName(JavaCodeCoverageListener.class))).isTrue();
-        assertThat(getProbes(JavaCodeCoverageListener.class, execData)).isEqualTo(fullyCovered);
+        assertThat(execData.contains(vmName(JavaCodeCoverageCollector.class))).isTrue();
+        assertThat(getProbes(JavaCodeCoverageCollector.class, execData)).isEqualTo(fullyCovered);
 
         boolean[] partiallyCovered = new boolean[PROBE_COUNT];
         partiallyCovered[0] = true;
-        assertThat(execData.contains(vmName(JavaCodeCoverageListenerTest.class))).isTrue();
-        assertThat(getProbes(JavaCodeCoverageListenerTest.class, execData))
+        assertThat(execData.contains(vmName(JavaCodeCoverageCollectorTest.class))).isTrue();
+        assertThat(getProbes(JavaCodeCoverageCollectorTest.class, execData))
                 .isEqualTo(partiallyCovered);
     }
 
     @Test
     public void testCoverageFlush_producesMultipleMeasurements() throws Exception {
+        enableJavaCoverage();
+
         List<String> coverageFileList =
                 ImmutableList.of(
                         "/data/misc/trace/com.android.test1.ec",
@@ -312,13 +341,57 @@
                 .when(mMockDevice)
                 .executeShellCommand("find /data/misc/trace -name '*.ec'");
 
-        mCodeCoverageListener.setCoverageFlusher(mMockFlusher);
+        mCodeCoverageCollector.setCoverageFlusher(mMockFlusher);
 
         // Simulate a test run.
-        mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
+        mCodeCoverageCollector.init(mMockContext, mFakeListener);
+        mCodeCoverageCollector.testRunStarted(RUN_NAME, TEST_COUNT);
         Map<String, String> metric = new HashMap<>();
         metric.put("coverageFilePath", DEVICE_PATH);
-        mCodeCoverageListener.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
+        mCodeCoverageCollector.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
+    }
+
+    @Test
+    public void testRunningProcess_coverageFileNotDeleted() throws Exception {
+        enableJavaCoverage();
+
+        List<String> coverageFileList =
+                ImmutableList.of(
+                        "/data/misc/trace/coverage1.ec",
+                        "/data/misc/trace/coverage2.ec",
+                        "/data/misc/trace/jacoco-123.mm.ec",
+                        "/data/misc/trace/jacoco-456.mm.ec");
+        String psOutput =
+                "USER       PID   PPID  VSZ   RSS   WCHAN       PC  S NAME\n"
+                        + "bluetooth   123  1366  123    456   SyS_epoll+   0  S com.android.bluetooth\n"
+                        + "radio       890     1 7890   123   binder_io+   0  S com.android.phone\n"
+                        + "root         11  1234  567   890   binder_io+   0  S not.a.java.package\n";
+
+        // Setup mocks.
+        mockCoverageFileOnDevice(DEVICE_PATH);
+
+        for (String additionalFile : coverageFileList) {
+            mockCoverageFileOnDevice(additionalFile);
+        }
+
+        doReturn("").when(mMockDevice).executeShellCommand("pm list packages -a");
+        doReturn(psOutput).when(mMockDevice).executeShellCommand("ps -e");
+        doReturn(String.join("\n", coverageFileList))
+                .when(mMockDevice)
+                .executeShellCommand("find /data/misc/trace -name '*.ec'");
+
+        // Simulate a test run.
+        mCodeCoverageCollector.init(mMockContext, mFakeListener);
+        mCodeCoverageCollector.testRunStarted(RUN_NAME, TEST_COUNT);
+        Map<String, String> metric = new HashMap<>();
+        metric.put("coverageFilePath", DEVICE_PATH);
+        mCodeCoverageCollector.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
+
+        // Verify the correct files were deleted and some files were not deleted.
+        verify(mMockDevice).deleteFile(coverageFileList.get(0));
+        verify(mMockDevice).deleteFile(coverageFileList.get(1));
+        verify(mMockDevice, never()).deleteFile(coverageFileList.get(2));
+        verify(mMockDevice).deleteFile(coverageFileList.get(3));
     }
 
     private void mockCoverageFileOnDevice(String devicePath)
@@ -369,13 +442,18 @@
 
     private static <T> boolean[] getProbes(Class<T> clazz, ExecutionDataStore execData)
             throws IOException {
-        return execData.get(classId(clazz), vmName(clazz), PROBE_COUNT).getProbes();
+        return execData.get(classId(clazz), vmName(clazz), PROBE_COUNT).getProbesCopy();
     }
 
     private static HashMap<String, Metric> createMetricsWithCoverageMeasurement(String devicePath) {
         return TfMetricProtoUtil.upgradeConvert(ImmutableMap.of("coverageFilePath", devicePath));
     }
 
+    private void enableJavaCoverage() throws ConfigurationException {
+        mCoverageOptionsSetter.setOptionValue("coverage", "true");
+        mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "JACOCO");
+    }
+
     /** An {@link ITestInvocationListener} which reads test log data streams for verification. */
     private static class LogFileReader implements ITestInvocationListener {
         /**
diff --git a/tests/src/com/android/tradefed/device/metric/MemInfoMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/MemInfoMetricCollectorTest.java
deleted file mode 100644
index c56a081..0000000
--- a/tests/src/com/android/tradefed/device/metric/MemInfoMetricCollectorTest.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.io.File;
-
-/** Unit tests for {@link MemInfoMetricCollector}. */
-// TODO(b/71868090): Consolidate all the individual metric collector tests into one common tests.
-@RunWith(JUnit4.class)
-public class MemInfoMetricCollectorTest {
-    @Mock IInvocationContext mContext;
-
-    @Mock ITestInvocationListener mListener;
-
-    @Mock ITestDevice mDevice;
-
-    @Spy MemInfoMetricCollector mMemInfoMetricCollector;
-
-    @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mMemInfoMetricCollector.init(mContext, mListener);
-
-        doNothing()
-                .when(mListener)
-                .testLog(
-                        anyString(), eq(LogDataType.COMPACT_MEMINFO), any(InputStreamSource.class));
-
-        doReturn(new File("compact-meminfo-1"))
-                .when(mMemInfoMetricCollector)
-                .saveProcessOutput(any(ITestDevice.class), anyString(), anyString());
-
-        doReturn(tempFolder.newFolder()).when(mMemInfoMetricCollector).createTempDir();
-    }
-
-    @Test
-    public void testCollect() throws Exception {
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-        when(mMemInfoMetricCollector.getFileSuffix()).thenReturn("1");
-
-        mMemInfoMetricCollector.collect(mDevice, runData);
-
-        // Verify that we logged the metric file.
-        verify(mListener).testLog(eq("compact-meminfo-1"), eq(LogDataType.COMPACT_MEMINFO), any());
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/PagetypeInfoMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/PagetypeInfoMetricCollectorTest.java
deleted file mode 100644
index 9348743..0000000
--- a/tests/src/com/android/tradefed/device/metric/PagetypeInfoMetricCollectorTest.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.io.File;
-
-/** Unit tests for {@link PagetypeInfoMetricCollector}. */
-// TODO(b/71868090): Consolidate all the individual metric collector tests into one common tests.
-@RunWith(JUnit4.class)
-public class PagetypeInfoMetricCollectorTest {
-    @Mock IInvocationContext mContext;
-
-    @Mock ITestInvocationListener mListener;
-
-    @Mock ITestDevice mDevice;
-
-    @Spy PagetypeInfoMetricCollector mPagetypeInfoMetricCollector;
-
-    @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mPagetypeInfoMetricCollector.init(mContext, mListener);
-
-        doNothing()
-                .when(mListener)
-                .testLog(anyString(), eq(LogDataType.TEXT), any(InputStreamSource.class));
-
-        doReturn(new File("pagetypeinfo-1"))
-                .when(mPagetypeInfoMetricCollector)
-                .saveProcessOutput(any(ITestDevice.class), anyString(), anyString());
-
-        doReturn(tempFolder.newFolder()).when(mPagetypeInfoMetricCollector).createTempDir();
-    }
-
-    @Test
-    public void testCollect() throws Exception {
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-        when(mPagetypeInfoMetricCollector.getFileSuffix()).thenReturn("1");
-
-        mPagetypeInfoMetricCollector.collect(mDevice, runData);
-
-        // Verify that we logged the metric file.
-        verify(mListener).testLog(eq("pagetypeinfo-1"), eq(LogDataType.TEXT), any());
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
index 24418a6..9d131c5 100644
--- a/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
@@ -16,6 +16,7 @@
 
 package com.android.tradefed.device.metric;
 
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.times;
 
@@ -120,6 +121,47 @@
         assertTrue("Trace duration metrics not available but expected.",
                 currentMetrics.get("perfetto_trace_extractor_runtime").getMeasurements()
                         .getSingleDouble() >= 0);
+        assertNull("Trace duration metrics not available but expected.",
+                currentMetrics.get("perfetto_trace_file_size_bytes"));
+    }
+
+    @Test
+    public void testProcessingFlowWithFileSizeMetric() throws Exception {
+
+        OptionSetter setter = new OptionSetter(mPerfettoMetricCollector);
+        setter.setOptionValue("pull-pattern-keys", "perfettofile");
+        setter.setOptionValue("perfetto-binary-path", "trx");
+        setter.setOptionValue("convert-metric-file", "false");
+        setter.setOptionValue("collect-perfetto-file-size", "true");
+        HashMap<String, Metric> currentMetrics = new HashMap<>();
+        currentMetrics.put("perfettofile", TfMetricProtoUtil.stringToMetric("/data/trace.pb"));
+        Mockito.when(mMockDevice.pullFile(Mockito.eq("/data/trace.pb")))
+                .thenReturn(new File("trace"));
+
+        TestDescription testDesc = new TestDescription("xyz", "abc");
+        CommandResult cr = new CommandResult();
+        cr.setStatus(CommandStatus.SUCCESS);
+        cr.setStdout("abc:efg");
+
+        Mockito.doReturn(cr).when(mPerfettoMetricCollector).runHostCommand(Mockito.anyLong(),
+                Mockito.any(), Mockito.any(), Mockito.any());
+
+        mPerfettoMetricCollector.testStarted(testDesc);
+        mPerfettoMetricCollector.testEnded(testDesc, currentMetrics);
+
+        Mockito.verify(mPerfettoMetricCollector).runHostCommand(Mockito.anyLong(),
+                Mockito.any(), Mockito.any(), Mockito.any());
+        Mockito.verify(mMockListener)
+                .testLog(Mockito.eq("trace"), Mockito.eq(LogDataType.PERFETTO), Mockito.any());
+        assertTrue("Expected two metrics that includes success status",
+                currentMetrics.get("perfetto_trace_extractor_status").getMeasurements()
+                        .getSingleString().equals("1"));
+        assertTrue("Trace duration metrics not available but expected.",
+                currentMetrics.get("perfetto_trace_extractor_runtime").getMeasurements()
+                        .getSingleDouble() >= 0);
+        assertTrue("Trace file size metric is not available in the final metrics.",
+                currentMetrics.get("perfetto_trace_file_size_bytes").getMeasurements()
+                        .getSingleDouble() >= 0);
     }
 
     @Test
@@ -328,3 +370,4 @@
     }
 
 }
+
diff --git a/tests/src/com/android/tradefed/device/metric/ProcessMaxMemoryCollectorTest.java b/tests/src/com/android/tradefed/device/metric/ProcessMaxMemoryCollectorTest.java
deleted file mode 100644
index b66138f..0000000
--- a/tests/src/com/android/tradefed/device/metric/ProcessMaxMemoryCollectorTest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.ITestInvocationListener;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mockito;
-
-import java.util.Collections;
-import java.util.HashMap;
-
-/** Unit tests for {@link ProcessMaxMemoryCollector}. */
-@RunWith(JUnit4.class)
-public class ProcessMaxMemoryCollectorTest {
-
-    private ProcessMaxMemoryCollector mCollector;
-    private IInvocationContext mContext;
-    private ITestDevice mDevice;
-    private ITestInvocationListener mListener;
-
-    private static final String TEST_INPUT =
-            "time,28506638,177086152\n"
-                    + "4,938,system_server,11,22,N/A,44,0,0,N/A,0,0,0,N/A,0,27613,14013,176602,"
-                    + "218228,0,0,122860,122860,1512,1412,5740,8664,0,0,154924,154924,27568,"
-                    + "13972,11916,53456,0,0,123008,123008,0,0,0,0,0,0,0,0,Dalvik Other,3662,0,"
-                    + "104,0,3660,0,0,0,Stack,1576,0,8,0,1576,0,0,0,Cursor,0,0,0,0,0,0,0,0,"
-                    + "Ashmem,156,0,20,0,148,0,0,0,Gfx dev,100,0,48,0,76,0,0,0,Other dev,116,0,"
-                    + "164,0,0,96,0,0,.so mmap,7500,2680,3984,21864,904,2680,0,0,.jar mmap,0,0,0,"
-                    + "0,0,0,0,0,.apk mmap,72398,71448,0,11736,0,71448,0,0,.ttf mmap,0,0,0,0,0,0,"
-                    + "0,0,.dex mmap,76874,46000,0,83644,40,46000,0,0,.oat mmap,8127,2684,64,"
-                    + "26652,0,2684,0,0,.art mmap,1991,48,972,10004,1544,48,0,0,Other mmap,137,0,"
-                    + "44,1024,4,52,0,0,EGL mtrack,0,0,0,0,0,0,0,0,GL mtrack,111,222,333,444,555,"
-                    + "666,777,888,";
-
-    @Before
-    public void setup() throws Exception {
-        mCollector = new ProcessMaxMemoryCollector();
-        mContext = mock(IInvocationContext.class);
-        mDevice = mock(ITestDevice.class);
-        when(mContext.getDevices()).thenReturn(Collections.singletonList(mDevice));
-        mListener = mock(ITestInvocationListener.class);
-        mCollector.init(mContext, mListener);
-        OptionSetter setter = new OptionSetter(mCollector);
-        setter.setOptionValue("memory-usage-process-name", "system_server");
-    }
-
-    @Test
-    public void testCollector() throws Exception {
-        when(mDevice.executeShellCommand(Mockito.eq("dumpsys meminfo --checkin system_server")))
-                .thenReturn(TEST_INPUT);
-
-        DeviceMetricData data = new DeviceMetricData(mContext);
-        mCollector.onStart(data);
-        mCollector.collect(mDevice, data);
-        mCollector.onEnd(data);
-
-        verify(mDevice).executeShellCommand(Mockito.eq("dumpsys meminfo --checkin system_server"));
-
-        HashMap<String, Metric> results = new HashMap<>();
-        data.addToMetrics(results);
-        assertEquals(218228, results.get("MAX_PSS#system_server").getMeasurements().getSingleInt());
-        assertEquals(53456, results.get("MAX_USS#system_server").getMeasurements().getSingleInt());
-    }
-
-    @Test
-    public void testCollectorNoProcess() throws Exception {
-        when(mDevice.executeShellCommand(Mockito.eq("dumpsys meminfo --checkin system_server")))
-                .thenReturn("No process found for: system_server");
-
-        DeviceMetricData data = new DeviceMetricData(mContext);
-        mCollector.onStart(data);
-        mCollector.collect(mDevice, data);
-        mCollector.onEnd(data);
-
-        verify(mDevice).executeShellCommand(Mockito.eq("dumpsys meminfo --checkin system_server"));
-
-        HashMap<String, Metric> results = new HashMap<>();
-        data.addToMetrics(results);
-        assertTrue(results.isEmpty());
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/ScheduleMultipleDeviceMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/ScheduleMultipleDeviceMetricCollectorTest.java
deleted file mode 100644
index 13605f3..0000000
--- a/tests/src/com/android/tradefed/device/metric/ScheduleMultipleDeviceMetricCollectorTest.java
+++ /dev/null
@@ -1,284 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.invoker.InvocationContext;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.util.RunUtil;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/** Unit tests for {@link ScheduleMultipleDeviceMetricCollector}. */
-@RunWith(JUnit4.class)
-public class ScheduleMultipleDeviceMetricCollectorTest {
-    @Rule public final TemporaryFolder folder = new TemporaryFolder();
-    @Mock private ITestDevice mTestDevice;
-    @Mock private ITestInvocationListener mMockListener;
-    @Spy private ScheduleMultipleDeviceMetricCollector mMultipleMetricCollector;
-
-    private IInvocationContext mContext;
-
-    static class TestMeminfoCollector extends ScheduledDeviceMetricCollector {
-        private int mInternalCounter = 0;
-        private String key = "meminfo";
-
-        TestMeminfoCollector() {
-            setTag("meminfoInterval");
-        }
-
-        @Override
-        public void collect(ITestDevice device, DeviceMetricData runData)
-                throws InterruptedException {
-            mInternalCounter++;
-            runData.addMetricForDevice(
-                    device,
-                    key + mInternalCounter,
-                    Metric.newBuilder()
-                            .setMeasurements(
-                                    Measurements.newBuilder()
-                                            .setSingleString("value" + mInternalCounter)));
-        }
-    }
-
-    static class TestJankinfoCollector extends ScheduledDeviceMetricCollector {
-        private int mInternalCounter = 0;
-        private String key = "jankinfo";
-
-        TestJankinfoCollector() {
-            setTag("jankInterval");
-        }
-
-        @Override
-        public void collect(ITestDevice device, DeviceMetricData runData)
-                throws InterruptedException {
-            mInternalCounter++;
-            runData.addMetricForDevice(
-                    device,
-                    key + mInternalCounter,
-                    Metric.newBuilder()
-                            .setMeasurements(
-                                    Measurements.newBuilder()
-                                            .setSingleString("value" + mInternalCounter)));
-        }
-    }
-
-    static class TestFragmentationCollector extends ScheduledDeviceMetricCollector {
-        private int mInternalCounter = 0;
-        private String key = "fragmentation";
-
-        TestFragmentationCollector() {
-            setTag("fragmentationInterval");
-        }
-
-        @Override
-        public void collect(ITestDevice device, DeviceMetricData runData)
-                throws InterruptedException {
-            mInternalCounter++;
-            runData.addMetricForDevice(
-                    device,
-                    key + mInternalCounter,
-                    Metric.newBuilder()
-                            .setMeasurements(
-                                    Measurements.newBuilder()
-                                            .setSingleString("value" + mInternalCounter)));
-        }
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        mContext = new InvocationContext();
-        mContext.addAllocatedDevice("test device", mTestDevice);
-    }
-
-    @Test
-    public void testMultipleMetricCollector_success() throws Exception {
-        OptionSetter setter = new OptionSetter(mMultipleMetricCollector);
-
-        // Set up the metric collection storage path.
-        File metricStoragePath = folder.newFolder();
-        setter.setOptionValue("metric-storage-path", metricStoragePath.toString());
-
-        // Set up the intervals.
-        Map<String, Long> intervals = new HashMap<>();
-        intervals.put("meminfoInterval", 100L);
-        intervals.put("fragmentationInterval", 100L);
-        intervals.put("jankInterval", 100L);
-        for (String key : intervals.keySet()) {
-            setter.setOptionValue(
-                    "metric-collection-intervals", key, intervals.get(key).toString());
-        }
-
-        // Request the collectors.
-        List<String> classnames = new ArrayList<>();
-        classnames.add(TestMeminfoCollector.class.getName());
-        classnames.add(TestJankinfoCollector.class.getName());
-        classnames.add(TestFragmentationCollector.class.getName());
-        for (String key : classnames) {
-            setter.setOptionValue("metric-collector-command-classes", key);
-        }
-
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-
-        // Start the tests.
-        HashMap<String, Metric> metrics = new HashMap<>();
-        mMultipleMetricCollector.init(mContext, mMockListener);
-        try {
-            mMultipleMetricCollector.onTestRunStart(runData);
-            RunUtil.getDefault().sleep(500);
-        } finally {
-            mMultipleMetricCollector.onTestRunEnd(runData, metrics);
-        }
-
-        // We give it 500msec to run and 100msec interval we should easily have at least run all the
-        // metrics once.
-        // assert that the metrics contains filenames of all the collected metrics.
-        HashMap<String, Metric> metricsCollected = new HashMap<>();
-        runData.addToMetrics(metricsCollected);
-
-        assertTrue(metricsCollected.containsKey("jankinfo1"));
-        assertTrue(metricsCollected.containsKey("meminfo1"));
-        assertTrue(metricsCollected.containsKey("fragmentation1"));
-    }
-
-    @Test
-    public void testMultipleMetricCollector_noFailureEvenIfNoCollectorRequested() throws Exception {
-        HashMap<String, Metric> metrics = new HashMap<>();
-        mMultipleMetricCollector.init(mContext, mMockListener);
-
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-
-        try {
-            mMultipleMetricCollector.onTestRunStart(runData);
-            RunUtil.getDefault().sleep(500);
-        } finally {
-            mMultipleMetricCollector.onTestRunEnd(runData, metrics);
-        }
-
-        // No metrics should have been collected.
-        HashMap<String, Metric> metricsCollected = new HashMap<>();
-        runData.addToMetrics(metricsCollected);
-
-        assertEquals(0, metricsCollected.size());
-    }
-
-    /** Test that if a specified collector does not exists, we ignore it and proceed. */
-    @Test
-    public void testMultipleMetricCollector_collectorNotFound() throws Exception {
-        OptionSetter setter = new OptionSetter(mMultipleMetricCollector);
-
-        // Set up the metric collection storage path.
-        File metricStoragePath = folder.newFolder();
-        setter.setOptionValue("metric-storage-path", metricStoragePath.toString());
-
-        // Set up the intervals.
-        Map<String, Long> intervals = new HashMap<>();
-        intervals.put("meminfoInterval", 100L);
-        for (String key : intervals.keySet()) {
-            setter.setOptionValue(
-                    "metric-collection-intervals", key, intervals.get(key).toString());
-        }
-
-        // Request the collectors.
-        List<String> classnames = new ArrayList<>();
-        classnames.add(TestMeminfoCollector.class.getName());
-        classnames.add("this.does.not.exists.collector");
-        for (String key : classnames) {
-            setter.setOptionValue("metric-collector-command-classes", key);
-        }
-
-        HashMap<String, Metric> metrics = new HashMap<>();
-        mMultipleMetricCollector.init(mContext, mMockListener);
-
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-
-        try {
-            mMultipleMetricCollector.onTestRunStart(runData);
-            RunUtil.getDefault().sleep(500);
-        } finally {
-            mMultipleMetricCollector.onTestRunEnd(runData, metrics);
-        }
-
-        // No metrics should have been collected.
-        HashMap<String, Metric> metricsCollected = new HashMap<>();
-        runData.addToMetrics(metricsCollected);
-
-        assertTrue(metricsCollected.containsKey("meminfo1"));
-    }
-
-    @Test
-    public void testMultipleMetricCollector_failsForNonNegativeInterval() throws Exception {
-        String expectedStderr =
-                "class com.android.tradefed.device.metric."
-                        + "ScheduleMultipleDeviceMetricCollectorTest$TestJankinfoCollector expects "
-                        + "a non negative interval.";
-
-        OptionSetter setter = new OptionSetter(mMultipleMetricCollector);
-
-        // Set up the metric collection storage path.
-        setter.setOptionValue("metric-storage-path", folder.newFolder().toString());
-
-        // Set up the interval.
-        Map<String, Long> intervals = new HashMap<>();
-        intervals.put("jankInterval", -100L);
-        for (String key : intervals.keySet()) {
-            setter.setOptionValue(
-                    "metric-collection-intervals", key, intervals.get(key).toString());
-        }
-
-        // Set up the classname.
-        List<String> classnames = new ArrayList<>();
-        classnames.add(TestJankinfoCollector.class.getName());
-        for (String key : classnames) {
-            setter.setOptionValue("metric-collector-command-classes", key);
-        }
-
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-
-        // Start the tests, which should fail with the expected error message.
-        mMultipleMetricCollector.init(mContext, mMockListener);
-
-        try {
-            mMultipleMetricCollector.onTestRunStart(runData);
-            fail("Should throw illegal argument exception in case of negative intervals.");
-        } catch (IllegalArgumentException e) {
-            assertEquals(expectedStderr, e.getMessage());
-        }
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/ScheduledDeviceMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/ScheduledDeviceMetricCollectorTest.java
deleted file mode 100644
index 1f23a43..0000000
--- a/tests/src/com/android/tradefed/device/metric/ScheduledDeviceMetricCollectorTest.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
-
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.invoker.InvocationContext;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.util.RunUtil;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mockito;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/** Unit tests for {@link ScheduledDeviceMetricCollector}. */
-@RunWith(JUnit4.class)
-public class ScheduledDeviceMetricCollectorTest {
-    private Map<String, ITestDevice> mDevicesWithNames = new HashMap<>();
-
-    public static class TestableAsyncTimer extends ScheduledDeviceMetricCollector {
-        private int mInternalCounter = 0;
-
-        @Override
-        void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-            mInternalCounter++;
-            runData.addMetricForDevice(
-                    device,
-                    "key" + mInternalCounter,
-                    Metric.newBuilder()
-                            .setMeasurements(
-                                    Measurements.newBuilder()
-                                            .setSingleString("value" + mInternalCounter)));
-        }
-    }
-
-    private TestableAsyncTimer mBase;
-    private IInvocationContext mContext;
-    private ITestInvocationListener mMockListener;
-
-    @Before
-    public void setUp() {
-        mBase = new TestableAsyncTimer();
-        mContext = new InvocationContext();
-        mMockListener = Mockito.mock(ITestInvocationListener.class);
-    }
-
-    /** Test the periodic run of the collector once testRunStarted has been called. */
-    @Test
-    public void testSetupAndPeriodicRunSingleDevice() throws Exception {
-        // Setup the context with the devices.
-        mDevicesWithNames.put("test device 1", mock(ITestDevice.class));
-        mContext.addAllocatedDevice(mDevicesWithNames);
-
-        OptionSetter setter = new OptionSetter(mBase);
-        // 100 ms interval
-        setter.setOptionValue("interval", "100");
-        HashMap<String, Metric> metrics = new HashMap<>();
-        mBase.init(mContext, mMockListener);
-        try {
-            mBase.testRunStarted("testRun", 1);
-            RunUtil.getDefault().sleep(500);
-        } finally {
-            mBase.testRunEnded(0l, metrics);
-        }
-        // We give it 500msec to run and 100msec interval we should easily have at least three
-        // iterations
-        assertTrue(metrics.containsKey("key1"));
-        assertTrue(metrics.containsKey("key2"));
-        assertTrue(metrics.containsKey("key3"));
-    }
-
-    /**
-     * Test the periodic run of the collector on multiple devices once testRunStarted has been
-     * called.
-     */
-    @Test
-    public void testSetupAndPeriodicRunMultipleDevices() throws Exception {
-        // Setup the context with the devices.
-        mDevicesWithNames.put("test device 1", mock(ITestDevice.class));
-        mDevicesWithNames.put("test device 2", mock(ITestDevice.class));
-        mContext.addAllocatedDevice(mDevicesWithNames);
-
-        OptionSetter setter = new OptionSetter(mBase);
-        // 100 ms interval
-        setter.setOptionValue("interval", "100");
-        HashMap<String, Metric> metrics = new HashMap<>();
-        mBase.init(mContext, mMockListener);
-        try {
-            mBase.testRunStarted("testRun", 1);
-            RunUtil.getDefault().sleep(500);
-        } finally {
-            mBase.testRunEnded(0l, metrics);
-        }
-        // We give it 500msec to run and 100msec interval we should easily have at least two
-        // iterations one for each device. The order of execution is arbitrary so check for prefix
-        // only.
-        assertTrue(metrics.keySet().stream().anyMatch(key -> key.startsWith("{test device 1}")));
-        assertTrue(metrics.keySet().stream().anyMatch(key -> key.startsWith("{test device 2}")));
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/TemperatureCollectorTest.java b/tests/src/com/android/tradefed/device/metric/TemperatureCollectorTest.java
deleted file mode 100644
index a4c8550..0000000
--- a/tests/src/com/android/tradefed/device/metric/TemperatureCollectorTest.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.ITestInvocationListener;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.Collections;
-import java.util.HashMap;
-
-/** Unit tests for {@link TemperatureCollector}. */
-@RunWith(JUnit4.class)
-public class TemperatureCollectorTest {
-
-    private TemperatureCollector mCollector;
-    private IInvocationContext mContext;
-    private ITestDevice mDevice;
-    private ITestInvocationListener mListener;
-
-    @Before
-    public void setup() throws Exception {
-        mCollector = new TemperatureCollector();
-        mContext = mock(IInvocationContext.class);
-        mDevice = mock(ITestDevice.class);
-        when(mDevice.isAdbRoot()).thenReturn(true);
-        when(mContext.getDevices()).thenReturn(Collections.singletonList(mDevice));
-        mListener = mock(ITestInvocationListener.class);
-        mCollector.init(mContext, mListener);
-        OptionSetter setter = new OptionSetter(mCollector);
-        setter.setOptionValue(
-                "device-temperature-file-path", "/sys/class/hwmon/hwmon1/device/msm_therm");
-    }
-
-    @Test
-    public void testCollector() throws Exception {
-        when(mDevice.executeShellCommand(eq("cat /sys/class/hwmon/hwmon1/device/msm_therm")))
-                .thenReturn("Result:32 Raw:7e51", "Result:22 Raw:7b51");
-
-        DeviceMetricData data = new DeviceMetricData(mContext);
-        mCollector.onStart(data);
-        mCollector.collect(mDevice, data);
-        mCollector.collect(mDevice, data);
-        mCollector.onEnd(data);
-
-        verify(mDevice, times(2))
-                .executeShellCommand(eq("cat /sys/class/hwmon/hwmon1/device/msm_therm"));
-
-        HashMap<String, Metric> results = new HashMap<>();
-        data.addToMetrics(results);
-        assertEquals(32D, results.get("max_temperature").getMeasurements().getSingleDouble(), 0);
-        assertEquals(22D, results.get("min_temperature").getMeasurements().getSingleDouble(), 0);
-    }
-
-    @Test
-    public void testCollectorNoData() throws Exception {
-        when(mDevice.executeShellCommand(eq("cat /sys/class/hwmon/hwmon1/device/msm_therm")))
-                .thenReturn(
-                        "cat: /sys/class/hwmon/hwmon1/device/msm_therm: No such file or directory");
-
-        DeviceMetricData data = new DeviceMetricData(mContext);
-        mCollector.onStart(data);
-        mCollector.collect(mDevice, data);
-        mCollector.onEnd(data);
-
-        verify(mDevice).executeShellCommand(eq("cat /sys/class/hwmon/hwmon1/device/msm_therm"));
-
-        HashMap<String, Metric> results = new HashMap<>();
-        data.addToMetrics(results);
-        assertTrue(results.isEmpty());
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/TraceMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/TraceMetricCollectorTest.java
deleted file mode 100644
index ffb3727..0000000
--- a/tests/src/com/android/tradefed/device/metric/TraceMetricCollectorTest.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.io.File;
-
-/** Unit tests for {@link TraceMetricCollector}. */
-// TODO(b/71868090): Consolidate all the individual metric collector tests into one common tests.
-@RunWith(JUnit4.class)
-public class TraceMetricCollectorTest {
-    @Mock IInvocationContext mContext;
-
-    @Mock ITestInvocationListener mListener;
-
-    @Mock ITestDevice mDevice;
-
-    @Spy TraceMetricCollector mTraceInfoMetricCollector;
-
-    @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mTraceInfoMetricCollector.init(mContext, mListener);
-
-        doNothing()
-                .when(mListener)
-                .testLog(anyString(), eq(LogDataType.TEXT), any(InputStreamSource.class));
-
-        doReturn(new File("trace-1"))
-                .when(mTraceInfoMetricCollector)
-                .saveProcessOutput(any(ITestDevice.class), anyString(), anyString());
-
-        doReturn(tempFolder.newFolder()).when(mTraceInfoMetricCollector).createTempDir();
-    }
-
-    @Test
-    public void testCollect() throws Exception {
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-        when(mTraceInfoMetricCollector.getFileSuffix()).thenReturn("1");
-
-        mTraceInfoMetricCollector.collect(mDevice, runData);
-
-        // Verify that we logged the metric file.
-        verify(mListener).testLog(eq("trace-1"), eq(LogDataType.TEXT), any());
-    }
-}
diff --git a/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java b/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
index dfb8b95..65e7c06 100644
--- a/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
+++ b/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
@@ -16,6 +16,8 @@
 package com.android.tradefed.invoker;
 
 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.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
@@ -425,4 +427,26 @@
             FileUtil.deleteFile(buildFile);
         }
     }
+
+    @Test
+    public void testBuildInfo_testTag() throws Exception {
+        IBuildInfo info = new BuildInfo();
+        assertEquals("stub", info.getTestTag());
+        File testsDir = FileUtil.createTempDir("doesnt_matter_testsdir");
+        try {
+            info.setFile(BuildInfoFileKey.TESTDIR_IMAGE, testsDir, "tests");
+            mContext.addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, info);
+            mConfig.getCommandOptions().setTestTag("test");
+            TestInformation testInfo =
+                    TestInformation.newBuilder().setInvocationContext(mContext).build();
+            assertNull(testInfo.executionFiles().get(FilesKey.TESTS_DIRECTORY));
+            mExecution.fetchBuild(testInfo, mConfig, null, null);
+            // Build test tag was updated
+            assertEquals("test", info.getTestTag());
+            // Execution file was back filled
+            assertNotNull(testInfo.executionFiles().get(FilesKey.TESTS_DIRECTORY));
+        } finally {
+            FileUtil.recursiveDelete(testsDir);
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
index b66ba56..768ae2c 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
@@ -174,15 +174,17 @@
         EasyMock.expect(mMockConfig.getLogOutput()).andStubReturn(mMockLogger);
         EasyMock.expect(mMockConfig.getConfigurationDescription()).andReturn(mConfigDesc);
         mMockLogger.init();
+        EasyMock.expectLastCall().times(2);
         EasyMock.expect(mMockLogger.getLog())
                 .andReturn(new ByteArrayInputStreamSource("fake".getBytes()));
         mMockLogger.closeLog();
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expectLastCall().times(3);
 
         mMockLogRegistry.registerLogger(mMockLogger);
+        EasyMock.expectLastCall().times(2);
         mMockLogRegistry.dumpToGlobalLog(mMockLogger);
         mMockLogRegistry.unregisterLogger();
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expectLastCall().times(3);
 
         EasyMock.expect(mMockConfig.getCommandLine()).andStubReturn("empty");
         EasyMock.expect(mMockConfig.getCommandOptions()).andStubReturn(new CommandOptions());
@@ -270,15 +272,17 @@
         EasyMock.expect(mMockConfig.getLogOutput()).andStubReturn(mMockLogger);
         EasyMock.expect(mMockConfig.getConfigurationDescription()).andReturn(mConfigDesc);
         mMockLogger.init();
+        EasyMock.expectLastCall().times(2);
         EasyMock.expect(mMockLogger.getLog())
                 .andReturn(new ByteArrayInputStreamSource("fake".getBytes()));
         mMockLogger.closeLog();
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expectLastCall().times(3);
 
         mMockLogRegistry.registerLogger(mMockLogger);
+        EasyMock.expectLastCall().times(2);
         mMockLogRegistry.dumpToGlobalLog(mMockLogger);
         mMockLogRegistry.unregisterLogger();
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expectLastCall().times(3);
 
         EasyMock.expect(mMockConfig.getCommandLine()).andStubReturn("empty");
         EasyMock.expect(mMockConfig.getCommandOptions()).andStubReturn(new CommandOptions());
@@ -352,15 +356,17 @@
         EasyMock.expect(mMockConfig.getLogOutput()).andStubReturn(mMockLogger);
         EasyMock.expect(mMockConfig.getConfigurationDescription()).andReturn(mConfigDesc);
         mMockLogger.init();
+        EasyMock.expectLastCall().times(2);
         EasyMock.expect(mMockLogger.getLog())
                 .andReturn(new ByteArrayInputStreamSource("fake".getBytes()));
         mMockLogger.closeLog();
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expectLastCall().times(3);
 
         mMockLogRegistry.registerLogger(mMockLogger);
+        EasyMock.expectLastCall().times(2);
         mMockLogRegistry.dumpToGlobalLog(mMockLogger);
         mMockLogRegistry.unregisterLogger();
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expectLastCall().times(3);
 
         EasyMock.expect(mMockConfig.getCommandLine()).andStubReturn("empty");
         EasyMock.expect(mMockConfig.getCommandOptions()).andStubReturn(new CommandOptions());
@@ -442,15 +448,17 @@
         EasyMock.expect(mMockConfig.getLogOutput()).andStubReturn(mMockLogger);
         EasyMock.expect(mMockConfig.getConfigurationDescription()).andReturn(mConfigDesc);
         mMockLogger.init();
+        EasyMock.expectLastCall().times(2);
         EasyMock.expect(mMockLogger.getLog())
                 .andReturn(new ByteArrayInputStreamSource("fake".getBytes()));
         mMockLogger.closeLog();
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expectLastCall().times(3);
 
         mMockLogRegistry.registerLogger(mMockLogger);
+        EasyMock.expectLastCall().times(2);
         mMockLogRegistry.dumpToGlobalLog(mMockLogger);
         mMockLogRegistry.unregisterLogger();
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expectLastCall().times(3);
 
         EasyMock.expect(mMockConfig.getCommandLine()).andStubReturn("empty");
         EasyMock.expect(mMockConfig.getCommandOptions()).andStubReturn(new CommandOptions());
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
index f8c4eb2..dc53f52 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
@@ -412,11 +412,6 @@
 
         setupMockFailureListeners(exception);
         setupInvoke();
-        EasyMock.reset(mMockLogger, mMockLogRegistry);
-        mMockLogRegistry.registerLogger(mMockLogger);
-        mMockLogger.init();
-        mMockLogger.closeLog();
-        mMockLogRegistry.unregisterLogger();
         IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
         CommandOptions cmdOptions = new CommandOptions();
         final String expectedTestTag = "TEST_TAG";
@@ -449,13 +444,6 @@
         setupMockFailureListenersAny(
                 new BuildRetrievalError("fake", InfraErrorIdentifier.ARTIFACT_DOWNLOAD_ERROR),
                 true);
-
-        EasyMock.reset(mMockLogger, mMockLogRegistry);
-        mMockLogRegistry.registerLogger(mMockLogger);
-        mMockLogger.init();
-        mMockLogger.closeLog();
-        mMockLogRegistry.unregisterLogger();
-
         EasyMock.expect(mMockLogger.getLog()).andReturn(EMPTY_STREAM_SOURCE);
         EasyMock.expect(mMockDevice.getLogcat()).andReturn(EMPTY_STREAM_SOURCE).times(2);
         mMockDevice.clearLogcat();
@@ -484,12 +472,6 @@
                         "No build found to test.", InfraErrorIdentifier.ARTIFACT_NOT_FOUND),
                 true);
 
-        EasyMock.reset(mMockLogger, mMockLogRegistry);
-        mMockLogRegistry.registerLogger(mMockLogger);
-        mMockLogger.init();
-        mMockLogger.closeLog();
-        mMockLogRegistry.unregisterLogger();
-
         EasyMock.expect(mMockLogger.getLog()).andReturn(EMPTY_STREAM_SOURCE);
         EasyMock.expect(mMockDevice.getLogcat()).andReturn(EMPTY_STREAM_SOURCE).times(2);
         mMockDevice.clearLogcat();
@@ -520,13 +502,6 @@
                         "No build found to test.", InfraErrorIdentifier.ARTIFACT_NOT_FOUND),
                 true, /* don't expect host log */
                 false);
-
-        EasyMock.reset(mMockLogger, mMockLogRegistry);
-        mMockLogRegistry.registerLogger(mMockLogger);
-        mMockLogger.init();
-        mMockLogger.closeLog();
-        EasyMock.expectLastCall().times(2);
-
         IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
         mStubConfiguration.setTest(test);
         // Host log fails to report
@@ -537,7 +512,7 @@
         Capture<IBuildInfo> captured = new Capture<>();
         mMockBuildProvider.cleanUp(EasyMock.capture(captured));
         mMockLogRegistry.unregisterLogger();
-        EasyMock.expectLastCall().times(2);
+        mMockLogger.closeLog();
         mMockLogRegistry.dumpToGlobalLog(mMockLogger);
         replayMocks(test, mockRescheduler);
         mTestInvocation.invoke(mStubInvocationMetadata, mStubConfiguration, mockRescheduler);
@@ -1149,9 +1124,12 @@
                 mMockTestListener.invocationFailed(EasyMock.<FailureDescription>anyObject());
                 mMockSummaryListener.invocationFailed(EasyMock.<FailureDescription>anyObject());
             } else {
+                FailureStatus failureStatus = FailureStatus.INFRA_FAILURE;
+                if (throwable instanceof BuildError) {
+                    failureStatus = FailureStatus.DEPENDENCY_ISSUE;
+                }
                 FailureDescription failure =
-                        FailureDescription.create(
-                                        throwable.getMessage(), FailureStatus.INFRA_FAILURE)
+                        FailureDescription.create(throwable.getMessage(), failureStatus)
                                 .setCause(throwable);
                 if (throwable instanceof BuildRetrievalError) {
                     failure.setActionInProgress(ActionInProgress.FETCHING_ARTIFACTS);
diff --git a/tests/src/com/android/tradefed/invoker/logger/InvocationLocalTest.java b/tests/src/com/android/tradefed/invoker/logger/InvocationLocalTest.java
index 7ce1e0d..d7a9b68 100644
--- a/tests/src/com/android/tradefed/invoker/logger/InvocationLocalTest.java
+++ b/tests/src/com/android/tradefed/invoker/logger/InvocationLocalTest.java
@@ -89,7 +89,7 @@
         Object value0 = invocation(() -> local.get());
         Object value1 = invocation(() -> local.get());
 
-        assertThat(value0).isNotSameAs(value1);
+        assertThat(value0).isNotSameInstanceAs(value1);
     }
 
     /**
diff --git a/tests/src/com/android/tradefed/invoker/shard/StrictShardHelperTest.java b/tests/src/com/android/tradefed/invoker/shard/StrictShardHelperTest.java
index 91b8e71..47a791f 100644
--- a/tests/src/com/android/tradefed/invoker/shard/StrictShardHelperTest.java
+++ b/tests/src/com/android/tradefed/invoker/shard/StrictShardHelperTest.java
@@ -20,6 +20,7 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.android.tradefed.build.BuildInfo;
 import com.android.tradefed.build.StubBuildProvider;
@@ -47,6 +48,7 @@
 import com.android.tradefed.testtype.suite.ITestSuite;
 import com.android.tradefed.util.FileUtil;
 
+import java.util.Arrays;
 import org.easymock.EasyMock;
 import org.junit.Assert;
 import org.junit.Before;
@@ -256,11 +258,36 @@
         }
     }
 
+    public class FakeStrictShardHelper extends StrictShardHelper {
+        List<IRemoteTest> fakeModules = new ArrayList<>();
+
+        public FakeStrictShardHelper(List<IRemoteTest> modules) {
+            fakeModules.addAll(modules);
+        }
+
+        @Override
+        protected List<List<IRemoteTest>> splitTests(List<IRemoteTest> fullList, int shardCount) {
+            List<List<IRemoteTest>> shards = new ArrayList<>();
+            shards.add(new ArrayList<>(fakeModules));
+            shards.add(new ArrayList<>(fakeModules));
+            return shards;
+        }
+    }
+
     private ITestSuite createFakeSuite(String name) throws Exception {
         ITestSuite suite = new SplitITestSuite(name);
         return suite;
     }
 
+    private ITestSuite createFakeSuite(String name, boolean intraModuleSharding) throws Exception {
+        ITestSuite suite = new SplitITestSuite(name);
+        if (!intraModuleSharding) {
+            OptionSetter setter = new OptionSetter(suite);
+            setter.setOptionValue("intra-module-sharding", "false");
+        }
+        return suite;
+    }
+
     private List<IRemoteTest> testShard(int shardIndex) throws Exception {
         mContext.addAllocatedDevice("default", EasyMock.createMock(ITestDevice.class));
         List<IRemoteTest> test = new ArrayList<>();
@@ -282,6 +309,26 @@
         return mConfig.getTests();
     }
 
+    private List<IRemoteTest> createITestSuiteList(List<String> modules) throws Exception {
+        List<IRemoteTest> tests = new ArrayList<>();
+        for (String name : modules) {
+            tests.add(createFakeSuite(name, false).split(2, mTestInfo).iterator().next());
+        }
+
+        CommandOptions options = new CommandOptions();
+        OptionSetter setter = new OptionSetter(options);
+        setter.setOptionValue("shard-count", "2");
+        setter.setOptionValue("shard-index", Integer.toString(1));
+        setter.setOptionValue("optimize-mainline-test", "true");
+        mConfig.setCommandOptions(options);
+        mConfig.setCommandLine(new String[] {"empty"});
+        mConfig.setTests(tests);
+
+        FakeStrictShardHelper fakeHelper = new FakeStrictShardHelper(tests);
+        fakeHelper.shardConfig(mConfig, mTestInfo, mRescheduler, null);
+        return mConfig.getTests();
+    }
+
     /**
      * Total for all the _shardX test should be 14 tests (2 per modules). 6 for module1: 3 module1
      * shard * 2 4 for module2: 2 module2 shard * 2 4 for module3: 2 module3 shard * 2
@@ -304,6 +351,61 @@
         assertEquals(1, ((ITestSuite) res.get(2)).getDirectModule().numTests());
     }
 
+    /**
+     * Test that the unsorted test modules are re-ordered.
+     */
+    @Test
+    public void testReorderTestModules() throws Exception {
+        List<String> unSortedModules =
+            Arrays.asList(
+                "module1[com.android.mod1.apex]",
+                "module1[com.android.mod1.apex+com.android.mod2.apex]",
+                "module2[com.android.mod1.apex]",
+                "module1[com.android.mod3.apk]",
+                "module2[com.android.mod1.apex+com.android.mod2.apex]",
+                "module2[com.android.mod3.apk]",
+                "module3[com.android.mod1.apex+com.android.mod2.apex]",
+                "module3[com.android.mod3.apk]",
+                "module4[com.android.mod3.apk]",
+                "module5[com.android.mod3.apk]"
+            );
+        List<IRemoteTest> res = createITestSuiteList(unSortedModules);
+
+        List<String> sortedModules =
+            Arrays.asList(
+                "module1[com.android.mod1.apex]",
+                "module2[com.android.mod1.apex]",
+                "module1[com.android.mod1.apex+com.android.mod2.apex]",
+                "module2[com.android.mod1.apex+com.android.mod2.apex]",
+                "module3[com.android.mod1.apex+com.android.mod2.apex]",
+                "module1[com.android.mod3.apk]",
+                "module2[com.android.mod3.apk]",
+                "module3[com.android.mod3.apk]",
+                "module4[com.android.mod3.apk]",
+                "module5[com.android.mod3.apk]"
+            );
+        for (int i = 0 ; i < sortedModules.size() ; i++) {
+            assertEquals(sortedModules.get(i), ((ITestSuite)res.get(i)).getDirectModule().getId());
+        }
+    }
+
+    /**
+     * Test that the there exist a module with invalid parameterized modules defined.
+     */
+    @Test
+    public void testReorderTestModulesWithUnexpectedMainlineModules() throws Exception {
+        List<String> modules = Arrays.asList("module1[com.mod1.apex]", "module1[com.mod1]");
+        try {
+            List<IRemoteTest> res = createITestSuiteList(modules);
+            fail("Should have thrown an exception.");
+        } catch (RuntimeException expected) {
+            // expected
+            assertTrue(expected.getMessage().contains(
+                    "Module: module1[com.mod1] doesn't match the pattern for mainline " +
+                        "modules. The pattern should end with apk/apex/apks."));
+        }
+    }
+
     @Test
     public void testMergeSuite_shard1() throws Exception {
         List<IRemoteTest> res = testShard(1);
diff --git a/tests/src/com/android/tradefed/monitoring/LabResourceDeviceMonitorTest.java b/tests/src/com/android/tradefed/monitoring/LabResourceDeviceMonitorTest.java
new file mode 100644
index 0000000..ee51dfd
--- /dev/null
+++ b/tests/src/com/android/tradefed/monitoring/LabResourceDeviceMonitorTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.monitoring;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class LabResourceDeviceMonitorTest {
+
+    private LabResourceDeviceMonitor mLabResourceDeviceMonitor;
+
+    @Before
+    public void setUp() {
+        mLabResourceDeviceMonitor = new LabResourceDeviceMonitor();
+    }
+
+    @Test
+    public void testServerStartAndShutdown() {
+        Assert.assertFalse(
+                "server should be empty before monitor run",
+                mLabResourceDeviceMonitor.getServer().isPresent());
+        mLabResourceDeviceMonitor.run();
+        Assert.assertTrue(
+                "server should present after monitor run",
+                mLabResourceDeviceMonitor.getServer().isPresent());
+        Assert.assertEquals(
+                LabResourceDeviceMonitor.DEFAULT_PORT,
+                mLabResourceDeviceMonitor.getServer().get().getPort());
+        mLabResourceDeviceMonitor.stop();
+        Assert.assertTrue(
+                "server should be shutdown after monitor stop",
+                mLabResourceDeviceMonitor.getServer().get().isShutdown());
+    }
+}
diff --git a/tests/src/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java b/tests/src/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java
index d83c343..82fc304 100644
--- a/tests/src/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java
+++ b/tests/src/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java
@@ -306,10 +306,11 @@
     }
 
     /**
-     * Test metrics enabled with key prefixing.
+     * Test metrics enabled with key and string value prefixing.
      */
     @Test
-    public void testParsingWithKeyPrefixing() throws ConfigurationException, IOException {
+    public void testParsingWithKeyAndStringValuePrefixing()
+            throws ConfigurationException, IOException {
         setupPerfettoMetricFile(METRIC_FILE_FORMAT.text, true);
         mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
         mOptionSetter.setOptionValue(KEY_PREFIX_OPTION,
@@ -320,8 +321,8 @@
                 PREFIX_OPTION_VALUE,
                 new LogFile(
                         perfettoMetricProtoFile.getAbsolutePath(), "some.url", LogDataType.TEXTPB));
-        Map<String, Metric.Builder> parsedMetrics =
-                mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
+        Map<String, Metric.Builder> parsedMetrics = mProcessor
+                .processRunMetricsAndLogs(new HashMap<>(), testLogs);
 
         assertMetricsContain(parsedMetrics,
                 "perfetto_android_hwui_metric-process_info-process_name-com.android.systemui-all_mem_min",
@@ -329,6 +330,28 @@
 
     }
 
+    /**
+     * Test metrics enabled with key and integer value prefixing.
+     */
+    @Test
+    public void testParsingWithKeyAndIntegerValuePrefixing()
+            throws ConfigurationException, IOException {
+        setupPerfettoMetricFile(METRIC_FILE_FORMAT.text, true);
+        mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
+        mOptionSetter.setOptionValue(KEY_PREFIX_OPTION,
+                "perfetto.protos.AndroidCpuMetric.CoreData.id");
+        mOptionSetter.setOptionValue(ALL_METRICS_OPTION, "true");
+        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
+                .processRunMetricsAndLogs(new HashMap<>(), testLogs);
+        assertMetricsContain(parsedMetrics, "perfetto_android_cpu-process_info-name-com.google."
+                + "android.apps.messaging-threads-name-BG Thread #1-core-id-1-metrics-runtime_ns",
+                14376405);
+    }
 
     /** Test the post processor can parse binary perfetto metric proto format. */
     @Test
@@ -515,6 +538,54 @@
                         "    all_mem_min: 15120269\n" +
                         "    all_mem_avg: 24468104.289592762\n" +
                         "  }\n" +
+                        "}"
+                        + "android_cpu {\n" +
+                        "  process_info {\n" +
+                        "    name: \"com.google.android.apps.messaging\"\n" +
+                        "    metrics {\n" +
+                        "      mcycles: 139\n" +
+                        "      runtime_ns: 639064902\n" +
+                        "      min_freq_khz: 576000\n" +
+                        "      max_freq_khz: 2016000\n" +
+                        "      avg_freq_khz: 324000\n" +
+                        "    }\n" +
+                        "    threads {\n" +
+                        "      name: \"BG Thread #1\"\n" +
+                        "      core {\n" +
+                        "        id: 0\n" +
+                        "        metrics {\n" +
+                        "          runtime_ns: 8371202\n" +
+                        "        }\n" +
+                        "      }\n" +
+                        "      core {\n" +
+                        "        id: 1\n" +
+                        "        metrics {\n" +
+                        "          mcycles: 0\n" +
+                        "          runtime_ns: 14376405\n" +
+                        "          min_freq_khz: 1785600\n" +
+                        "          max_freq_khz: 1785600\n" +
+                        "          avg_freq_khz: 57977\n" +
+                        "        }\n" +
+                        "      }\n" +
+                        "      metrics {\n" +
+                        "        mcycles: 0\n" +
+                        "        runtime_ns: 22747607\n" +
+                        "        min_freq_khz: 1785600\n" +
+                        "        max_freq_khz: 1785600\n" +
+                        "        avg_freq_khz: 36000\n" +
+                        "      }\n" +
+                        "      core_type {\n" +
+                        "        type: \"little\"\n" +
+                        "        metrics {\n" +
+                        "          mcycles: 0\n" +
+                        "          runtime_ns: 22747607\n" +
+                        "          min_freq_khz: 1785600\n" +
+                        "          max_freq_khz: 1785600\n" +
+                        "          avg_freq_khz: 36000\n" +
+                        "        }\n" +
+                        "      }\n" +
+                        "    }\n" +
+                        " }\n" +
                         "}";
         FileWriter fileWriter = null;
         try {
@@ -593,3 +664,4 @@
                                                                         .getSingleString())));
     }
 }
+
diff --git a/tests/src/com/android/tradefed/presubmit/GeneralTestsConfigValidation.java b/tests/src/com/android/tradefed/presubmit/GeneralTestsConfigValidation.java
index ff0692e..852a4df 100644
--- a/tests/src/com/android/tradefed/presubmit/GeneralTestsConfigValidation.java
+++ b/tests/src/com/android/tradefed/presubmit/GeneralTestsConfigValidation.java
@@ -80,6 +80,8 @@
                             "com.android.tradefed.testtype.rust.RustBinaryTest",
                             "com.android.tradefed.testtype.StubTest",
                             "com.android.tradefed.testtype.ArtRunTest",
+                            "com.android.tradefed.testtype.ArtGTest",
+                            "com.android.tradefed.testtype.mobly.MoblyBinaryHostTest",
                             // Others
                             "com.google.android.deviceconfig.RebootTest"));
 
diff --git a/tests/src/com/android/tradefed/result/LogcatCrashResultForwarderTest.java b/tests/src/com/android/tradefed/result/LogcatCrashResultForwarderTest.java
index a902fd2..583a4c0 100644
--- a/tests/src/com/android/tradefed/result/LogcatCrashResultForwarderTest.java
+++ b/tests/src/com/android/tradefed/result/LogcatCrashResultForwarderTest.java
@@ -19,6 +19,7 @@
 
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 
 import org.easymock.Capture;
 import org.easymock.EasyMock;
@@ -52,7 +53,9 @@
         mMockListener.testStarted(test, 0L);
         EasyMock.expect(mMockDevice.getLogcatSince(0L))
                 .andReturn(new ByteArrayInputStreamSource("".getBytes()));
-        mMockListener.testFailed(test, "instrumentation failed. reason: 'Process crashed.'");
+        mMockListener.testFailed(
+                test,
+                FailureDescription.create("instrumentation failed. reason: 'Process crashed.'"));
         mMockListener.testEnded(test, 5L, new HashMap<String, Metric>());
 
         EasyMock.replay(mMockListener, mMockDevice);
@@ -89,15 +92,12 @@
         EasyMock.expect(mMockDevice.getLogcatSince(0L))
                 .andReturn(new ByteArrayInputStreamSource(logcat.getBytes()));
         // Some crash was added to the failure
-        mMockListener.testFailed(
-                EasyMock.eq(test),
-                EasyMock.contains(
-                        "instrumentation failed. reason: 'Process crashed.'"
-                                + "\nCrash Message:Runtime"));
+        Capture<FailureDescription> captured_1 = new Capture<>();
+        mMockListener.testFailed(EasyMock.eq(test), EasyMock.capture(captured_1));
         mMockListener.testEnded(test, 5L, new HashMap<String, Metric>());
         // If a run failure follows, expect it to contain the additional stack too.
-        Capture<FailureDescription> captured = new Capture<>();
-        mMockListener.testRunFailed(EasyMock.capture(captured));
+        Capture<FailureDescription> captured_2 = new Capture<>();
+        mMockListener.testRunFailed(EasyMock.capture(captured_2));
 
         EasyMock.replay(mMockListener, mMockDevice);
         mReporter.testStarted(test, 0L);
@@ -106,7 +106,16 @@
         mReporter.testRunFailed("Something went wrong.");
         EasyMock.verify(mMockListener, mMockDevice);
         assertTrue(
-                captured.getValue()
+                captured_1
+                        .getValue()
+                        .getErrorMessage()
+                        .contains(
+                                "instrumentation failed. reason: 'Process crashed.'"
+                                        + "\nCrash Message:Runtime"));
+        assertTrue(FailureStatus.TEST_FAILURE.equals(captured_1.getValue().getFailureStatus()));
+        assertTrue(
+                captured_2
+                        .getValue()
                         .getErrorMessage()
                         .contains("Something went wrong.\nCrash Message:Runtime"));
     }
@@ -138,7 +147,7 @@
         EasyMock.expect(mMockDevice.getLogcatSince(0L))
                 .andReturn(new ByteArrayInputStreamSource(logcat.getBytes()));
         // No crash added at the point of testFailed.
-        mMockListener.testFailed(test, "Something went wrong.");
+        mMockListener.testFailed(test, FailureDescription.create("Something went wrong."));
         mMockListener.testEnded(test, 5L, new HashMap<String, Metric>());
         // If a run failure comes with a crash detected, expect it to contain the additional stack.
         Capture<FailureDescription> captured = new Capture<>();
@@ -186,7 +195,7 @@
         EasyMock.expect(mMockDevice.getLogcatSince(0L))
                 .andReturn(new ByteArrayInputStreamSource(logcat.getBytes()));
         // No crash added at the point of testFailed.
-        mMockListener.testFailed(test, "Something went wrong.");
+        mMockListener.testFailed(test, FailureDescription.create("Something went wrong."));
         mMockListener.testEnded(test, 5L, new HashMap<String, Metric>());
         // If a run failure comes with a crash detected, expect it to contain the additional stack.
         Capture<FailureDescription> captured = new Capture<>();
@@ -207,4 +216,56 @@
                                         + "\tat class.method1(Class.java:1)\n"
                                         + "\tat class.method2(Class.java:2)\n"));
     }
+
+    /** Test that test-timeout tests have failure status TIMED_OUT. */
+    @Test
+    @SuppressWarnings("MustBeClosedChecker")
+    public void testTestTimedOutTests() {
+        String trace =
+                "org.junit.runners.model.TestTimedOutException: "
+                        + "test timed out after 1000 milliseconds";
+        mReporter = new LogcatCrashResultForwarder(mMockDevice, mMockListener);
+        TestDescription test = new TestDescription("com.class", "test");
+
+        mMockListener.testStarted(test, 0L);
+
+        Capture<FailureDescription> captured = new Capture<>();
+        mMockListener.testFailed(EasyMock.eq(test), EasyMock.capture(captured));
+        mMockListener.testEnded(test, 5L, new HashMap<String, Metric>());
+
+        EasyMock.replay(mMockListener, mMockDevice);
+        mReporter.testStarted(test, 0L);
+        mReporter.testFailed(test, trace);
+        mReporter.testEnded(test, 5L, new HashMap<String, Metric>());
+        EasyMock.verify(mMockListener, mMockDevice);
+        assertTrue(captured.getValue().getErrorMessage().contains(trace));
+        assertTrue(FailureStatus.TIMED_OUT.equals(captured.getValue().getFailureStatus()));
+    }
+
+    /** Test that shell-timeout tests have failure status TIMED_OUT. */
+    @Test
+    @SuppressWarnings("MustBeClosedChecker")
+    public void testShellTimedOutTests() {
+        String trace =
+                "Test failed to run to completion. "
+                        + " Reason: 'Failed to receive adb shell test output within 3000 ms. "
+                        + "Test may have timed out, or adb connection to device became "
+                        + "unresponsive'. Check device logcat for details";
+        mReporter = new LogcatCrashResultForwarder(mMockDevice, mMockListener);
+        TestDescription test = new TestDescription("com.class", "test");
+
+        mMockListener.testStarted(test, 0L);
+
+        Capture<FailureDescription> captured = new Capture<>();
+        mMockListener.testFailed(EasyMock.eq(test), EasyMock.capture(captured));
+        mMockListener.testEnded(test, 5L, new HashMap<String, Metric>());
+
+        EasyMock.replay(mMockListener, mMockDevice);
+        mReporter.testStarted(test, 0L);
+        mReporter.testFailed(test, trace);
+        mReporter.testEnded(test, 5L, new HashMap<String, Metric>());
+        EasyMock.verify(mMockListener, mMockDevice);
+        assertTrue(captured.getValue().getErrorMessage().contains(trace));
+        assertTrue(FailureStatus.TIMED_OUT.equals(captured.getValue().getFailureStatus()));
+    }
 }
diff --git a/tests/src/com/android/tradefed/result/error/ErrorIdentifierTest.java b/tests/src/com/android/tradefed/result/error/ErrorIdentifierTest.java
index 63556ab..b7cc8eb 100644
--- a/tests/src/com/android/tradefed/result/error/ErrorIdentifierTest.java
+++ b/tests/src/com/android/tradefed/result/error/ErrorIdentifierTest.java
@@ -37,6 +37,7 @@
         List<ErrorIdentifier> errors = new ArrayList<>();
         errors.addAll(Arrays.asList(InfraErrorIdentifier.values()));
         errors.addAll(Arrays.asList(DeviceErrorIdentifier.values()));
+        errors.addAll(Arrays.asList(TestErrorIdentifier.values()));
 
         List<String> names = errors.stream().map(e -> e.name()).collect(Collectors.toList());
         Set<String> uniques = new HashSet<>();
diff --git a/tests/src/com/android/tradefed/retry/ResultAggregatorTest.java b/tests/src/com/android/tradefed/retry/ResultAggregatorTest.java
index 43aaa7a..bd17c87 100644
--- a/tests/src/com/android/tradefed/retry/ResultAggregatorTest.java
+++ b/tests/src/com/android/tradefed/retry/ResultAggregatorTest.java
@@ -211,6 +211,135 @@
     }
 
     @Test
+    public void testForwarding_assumptionFailure() {
+        mDetailedListener = EasyMock.createStrictMock(ITestDetailedReceiver.class);
+        LogFile test1Log = new LogFile("test1", "url", LogDataType.TEXT);
+        LogFile test2LogBefore = new LogFile("test2-before", "url", LogDataType.TEXT);
+        LogFile test2LogAfter = new LogFile("test2-after", "url", LogDataType.TEXT);
+        LogFile testRun1LogBefore = new LogFile("test-run1-before", "url", LogDataType.TEXT);
+        LogFile testRun1LogAfter = new LogFile("test-run1-after", "url", LogDataType.TEXT);
+        LogFile beforeEnd = new LogFile("path", "url", LogDataType.TEXT);
+        LogFile betweenAttemptsLog = new LogFile("between-attempts", "url", LogDataType.TEXT);
+        LogFile moduleLog = new LogFile("module-log", "url", LogDataType.TEXT);
+        TestDescription test1 = new TestDescription("classname", "test1");
+        TestDescription test2 = new TestDescription("classname", "test2");
+        ILogSaver logger = EasyMock.createMock(ILogSaver.class);
+
+        EasyMock.expect(mDetailedListener.supportGranularResults()).andStubReturn(true);
+
+        // Invocation level
+        mAggListener.setLogSaver(logger);
+        mAggListener.invocationStarted(mInvocationContext);
+        EasyMock.expect(mAggListener.getSummary()).andStubReturn(null);
+        mDetailedListener.setLogSaver(logger);
+        mDetailedListener.invocationStarted(mInvocationContext);
+        EasyMock.expect(mDetailedListener.getSummary()).andStubReturn(null);
+
+        mAggListener.testModuleStarted(mModuleContext);
+        mDetailedListener.testModuleStarted(mModuleContext);
+
+        // Detailed receives the breakdown
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mDetailedListener.logAssociation("test1-log", test1Log);
+        mDetailedListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mDetailedListener.logAssociation("test2-before-log", test2LogBefore);
+        mDetailedListener.testFailed(test2, FailureDescription.create("I failed. retry me."));
+        mDetailedListener.logAssociation("test2-after-log", test2LogAfter);
+        mDetailedListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.logAssociation("test-run1-before-log", testRun1LogBefore);
+        mDetailedListener.logAssociation("test-run1-after-log", testRun1LogAfter);
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(1), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mDetailedListener.testAssumptionFailure(
+                EasyMock.eq(test2), EasyMock.eq(FailureDescription.create("Assump failure")));
+        mDetailedListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+        mDetailedListener.logAssociation("between-attempts", betweenAttemptsLog);
+        mDetailedListener.logAssociation("module-log", moduleLog);
+
+        // Aggregated listeners receives the aggregated results
+        mAggListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+        mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mAggListener.logAssociation("test1-log", test1Log);
+        mAggListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mAggListener.testAssumptionFailure(
+                EasyMock.eq(test2), (FailureDescription) EasyMock.anyObject());
+        mAggListener.logAssociation("test2-before-log", test2LogBefore);
+        mAggListener.logAssociation("test2-after-log", test2LogAfter);
+        mAggListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.logAssociation("test-run1-before-log", testRun1LogBefore);
+        mAggListener.logAssociation("test-run1-after-log", testRun1LogAfter);
+        mAggListener.testRunEnded(450L, new HashMap<String, Metric>());
+        mAggListener.logAssociation("between-attempts", betweenAttemptsLog);
+        mAggListener.logAssociation("module-log", moduleLog);
+        mAggListener.testModuleEnded();
+        mDetailedListener.testModuleEnded();
+        mAggListener.logAssociation("before-end", beforeEnd);
+        mAggListener.invocationEnded(500L);
+        mDetailedListener.logAssociation("before-end", beforeEnd);
+        mDetailedListener.invocationEnded(500L);
+
+        EasyMock.replay(mAggListener, mDetailedListener);
+        mAggregator =
+                new TestableResultAggregator(
+                        Arrays.asList(mAggListener, mDetailedListener),
+                        RetryStrategy.RETRY_ANY_FAILURE);
+        mAggregator.setLogSaver(logger);
+        mAggregator.invocationStarted(mInvocationContext);
+        mAggregator.testModuleStarted(mModuleContext);
+        // Attempt 1
+        mAggregator.testRunStarted("run1", 2, 0);
+        mAggregator.testStarted(test1);
+        mAggregator.logAssociation("test1-log", test1Log);
+        mAggregator.testEnded(test1, new HashMap<String, Metric>());
+        mAggregator.testStarted(test2);
+        mAggregator.logAssociation("test2-before-log", test2LogBefore);
+        mAggregator.testFailed(test2, FailureDescription.create("I failed. retry me."));
+        mAggregator.logAssociation("test2-after-log", test2LogAfter);
+        mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.logAssociation("test-run1-before-log", testRun1LogBefore);
+        mAggregator.testRunFailed("run fail");
+        mAggregator.logAssociation("test-run1-after-log", testRun1LogAfter);
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+        mAggregator.logAssociation("between-attempts", betweenAttemptsLog);
+        // Attempt 2
+        mAggregator.testRunStarted("run1", 2, 1);
+        mAggregator.testStarted(test2);
+        mAggregator.testAssumptionFailure(test2, FailureDescription.create("Assump failure"));
+        mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+        mAggregator.logAssociation("module-log", moduleLog);
+        mAggregator.testModuleEnded();
+        mAggregator.logAssociation("before-end", beforeEnd);
+        mAggregator.invocationEnded(500L);
+        EasyMock.verify(mAggListener, mDetailedListener);
+        assertEquals("run fail", mAggregator.getInvocationMetricRunError());
+    }
+
+    @Test
     public void testForwarding_runFailure() {
         mDetailedListener = EasyMock.createStrictMock(ITestDetailedReceiver.class);
         TestDescription test1 = new TestDescription("classname", "test1");
diff --git a/tests/src/com/android/tradefed/sandbox/SandboxedInvocationExecutionTest.java b/tests/src/com/android/tradefed/sandbox/SandboxedInvocationExecutionTest.java
deleted file mode 100644
index 7594ce4..0000000
--- a/tests/src/com/android/tradefed/sandbox/SandboxedInvocationExecutionTest.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.sandbox;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-
-import com.android.tradefed.build.BuildInfo;
-import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.config.Configuration;
-import com.android.tradefed.config.ConfigurationDef;
-import com.android.tradefed.config.IConfiguration;
-import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.invoker.InvocationContext;
-import com.android.tradefed.invoker.TestInformation;
-import com.android.tradefed.invoker.sandbox.SandboxedInvocationExecution;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.io.File;
-
-/** Unit tests for {@link SandboxedInvocationExecution}. */
-@RunWith(JUnit4.class)
-public class SandboxedInvocationExecutionTest {
-
-    private SandboxedInvocationExecution mExecution;
-    private IInvocationContext mContext;
-    private IConfiguration mConfig;
-
-    @Before
-    public void setUp() {
-        mExecution = new SandboxedInvocationExecution();
-        mContext = new InvocationContext();
-        mConfig = new Configuration("name", "desc");
-        mConfig.getConfigurationDescription().setSandboxed(true);
-    }
-
-    @Test
-    public void testBuildInfo_testTag() throws Exception {
-        IBuildInfo info = new BuildInfo();
-        assertEquals("stub", info.getTestTag());
-        info.setFile(BuildInfoFileKey.TESTDIR_IMAGE, new File("doesnt_matter_testsdir"), "tests");
-        mContext.addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, info);
-        mConfig.getCommandOptions().setTestTag("test");
-        TestInformation testInfo =
-                TestInformation.newBuilder().setInvocationContext(mContext).build();
-        assertNull(testInfo.executionFiles().get(FilesKey.TESTS_DIRECTORY));
-        mExecution.fetchBuild(testInfo, mConfig, null, null);
-        // Build test tag was updated
-        assertEquals("test", info.getTestTag());
-        // Execution file was back filled
-        assertNotNull(testInfo.executionFiles().get(FilesKey.TESTS_DIRECTORY));
-    }
-}
diff --git a/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java b/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
index a32f9b6..d58167f 100644
--- a/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
+++ b/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
@@ -266,6 +266,22 @@
         EasyMock.verify(mMockDevice);
     }
 
+    public void testSetup_wifi_network_empty() throws Exception {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doSettingExpectations("global", "wifi_on", "1");
+        doCommandsExpectations("svc wifi enable");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setWifiNetwork("");
+        mDeviceSetup.setWifiPsk("psk");
+
+        mDeviceSetup.setWifi(BinaryState.ON);
+        mDeviceSetup.setUp(mTestInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
     public void testSetup_wifi_multiple_network_names() throws Exception {
         doSetupExpectations();
         doCheckExternalStoreSpaceExpectations();
@@ -861,6 +877,9 @@
         doCheckExternalStoreSpaceExpectations();
         EasyMock.expect(mMockDevice.setProperty("persist.sys.timezone", "America/Los_Angeles"))
                 .andReturn(true);
+        EasyMock.expect(mMockDevice.getProperty("persist.sys.timezone"))
+                .andReturn("America/Los_Angeles")
+                .anyTimes();
         EasyMock.replay(mMockDevice);
 
         mDeviceSetup.setTimezone("America/Los_Angeles");
@@ -963,6 +982,9 @@
         Capture<String> setPropCapture = new Capture<>();
         doSetupExpectations(true, setPropCapture);
         doCheckExternalStoreSpaceExpectations();
+        EasyMock.expect(mMockDevice.getProperty("dalvik.vm.dexopt-flags"))
+                .andReturn("v=n")
+                .anyTimes();
         EasyMock.replay(mMockDevice);
 
         mDeviceSetup.setDisableDalvikVerifier(true);
@@ -1031,6 +1053,7 @@
         Capture<String> setPropCapture = new Capture<>();
         doSetupExpectations(true, setPropCapture);
         doCheckExternalStoreSpaceExpectations();
+        EasyMock.expect(mMockDevice.getProperty("key")).andReturn("value").anyTimes();
         EasyMock.replay(mMockDevice);
 
         mDeviceSetup.setDeprecatedAudioSilent(false);
@@ -1104,6 +1127,7 @@
         EasyMock.expect(mMockDevice.pushFile(f, "/data/local.prop")).andReturn(true).once();
         mMockDevice.reboot();
         EasyMock.expectLastCall().once();
+        EasyMock.expect(mMockDevice.getProperty("key")).andReturn("value").anyTimes();
 
         EasyMock.replay(mMockDevice);
 
@@ -1122,6 +1146,7 @@
         mMockDevice.deleteFile("/data/local.prop");
         mMockDevice.reboot();
         EasyMock.expectLastCall().once();
+        EasyMock.expect(mMockDevice.getProperty("key")).andReturn("value").anyTimes();
 
         EasyMock.replay(mMockDevice);
 
@@ -1342,7 +1367,16 @@
         if (testHarness) {
             EasyMock.expect(mMockDevice.setProperty("persist.sys.test_harness", "1"))
                     .andReturn(true);
+            EasyMock.expect(mMockDevice.getProperty("persist.sys.test_harness"))
+                    .andReturn("1")
+                    .anyTimes();
         }
+        EasyMock.expect(mMockDevice.getProperty("ro.telephony.disable-call"))
+                .andReturn("true")
+                .anyTimes();
+        EasyMock.expect(mMockDevice.getProperty("ro.audio.silent")).andReturn("1").anyTimes();
+        EasyMock.expect(mMockDevice.getProperty("ro.test_harness")).andReturn("1").anyTimes();
+        EasyMock.expect(mMockDevice.getProperty("ro.monkey")).andReturn("1").anyTimes();
     }
 
     /**
diff --git a/tests/src/com/android/tradefed/targetprep/DynamicSystemPreparerTest.java b/tests/src/com/android/tradefed/targetprep/DynamicSystemPreparerTest.java
index 9b38ce6..e889441 100644
--- a/tests/src/com/android/tradefed/targetprep/DynamicSystemPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/DynamicSystemPreparerTest.java
@@ -28,10 +28,9 @@
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.ZipUtil;
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
+
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -40,6 +39,10 @@
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+
 /** Unit tests for {@link DynamicSystemPreparer}. */
 @RunWith(JUnit4.class)
 public class DynamicSystemPreparerTest {
@@ -95,15 +98,12 @@
         }
     }
 
-    @Test
-    public void testSetUp() throws TargetSetupError, BuildError, DeviceNotAvailableException {
-        Mockito.when(mMockDevice.pushFile(Mockito.any(), Mockito.eq("/sdcard/system.raw.gz")))
-                .thenReturn(Boolean.TRUE);
+    private void mockGsiToolStatus(String status) throws DeviceNotAvailableException {
         doAnswer(
                         new Answer<Object>() {
                             @Override
                             public Object answer(InvocationOnMock invocation) {
-                                byte[] outputBytes = "running".getBytes();
+                                byte[] outputBytes = status.getBytes();
                                 ((CollectingOutputReceiver) invocation.getArguments()[1])
                                         .addOutput(outputBytes, 0, outputBytes.length);
                                 return null;
@@ -112,10 +112,62 @@
                 .when(mMockDevice)
                 .executeShellCommand(
                         matches("gsi_tool status"), any(CollectingOutputReceiver.class));
+    }
+
+    @Test
+    public void testSetUp() throws TargetSetupError, BuildError, DeviceNotAvailableException {
+        Mockito.when(mMockDevice.pushFile(Mockito.any(), Mockito.eq("/sdcard/system.raw.gz")))
+                .thenReturn(Boolean.TRUE);
+        Mockito.when(mMockDevice.waitForDeviceNotAvailable(Mockito.anyLong())).thenReturn(true);
+        mockGsiToolStatus("running");
         CommandResult res = new CommandResult();
         res.setStdout("");
         res.setStatus(CommandStatus.SUCCESS);
         Mockito.when(mMockDevice.executeShellV2Command("gsi_tool enable")).thenReturn(res);
         mPreparer.setUp(mMockDevice, mBuildInfo);
     }
+
+    @Test
+    public void testSetUp_installationFail() throws BuildError, DeviceNotAvailableException {
+        Mockito.when(mMockDevice.pushFile(Mockito.any(), Mockito.eq("/sdcard/system.raw.gz")))
+                .thenReturn(Boolean.TRUE);
+        Mockito.when(mMockDevice.waitForDeviceNotAvailable(Mockito.anyLong())).thenReturn(false);
+        try {
+            mPreparer.setUp(mMockDevice, mBuildInfo);
+            Assert.fail("setUp() should have thrown.");
+        } catch (TargetSetupError e) {
+            Assert.assertEquals(
+                    "Timed out waiting for DSU installation to complete and reboot",
+                    e.getMessage());
+        }
+    }
+
+    @Test
+    public void testSetUp_rebootFail() throws BuildError, DeviceNotAvailableException {
+        Mockito.when(mMockDevice.pushFile(Mockito.any(), Mockito.eq("/sdcard/system.raw.gz")))
+                .thenReturn(Boolean.TRUE);
+        Mockito.when(mMockDevice.waitForDeviceNotAvailable(Mockito.anyLong())).thenReturn(true);
+        Mockito.doThrow(new DeviceNotAvailableException()).when(mMockDevice).waitForDeviceOnline();
+        try {
+            mPreparer.setUp(mMockDevice, mBuildInfo);
+            Assert.fail("setUp() should have thrown.");
+        } catch (TargetSetupError e) {
+            Assert.assertEquals("Timed out booting into DSU", e.getMessage());
+        }
+    }
+
+    @Test
+    public void testSetUp_noDsuRunningAfterRebootFail()
+            throws BuildError, DeviceNotAvailableException {
+        Mockito.when(mMockDevice.pushFile(Mockito.any(), Mockito.eq("/sdcard/system.raw.gz")))
+                .thenReturn(Boolean.TRUE);
+        Mockito.when(mMockDevice.waitForDeviceNotAvailable(Mockito.anyLong())).thenReturn(true);
+        mockGsiToolStatus("normal");
+        try {
+            mPreparer.setUp(mMockDevice, mBuildInfo);
+            Assert.fail("setUp() should have thrown.");
+        } catch (TargetSetupError e) {
+            Assert.assertEquals("Failed to boot into DSU", e.getMessage());
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/targetprep/GsiDeviceFlashPreparerTest.java b/tests/src/com/android/tradefed/targetprep/GsiDeviceFlashPreparerTest.java
index f91253e..bccf68d 100644
--- a/tests/src/com/android/tradefed/targetprep/GsiDeviceFlashPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/GsiDeviceFlashPreparerTest.java
@@ -178,21 +178,36 @@
         EasyMock.verify(mMockDevice, mMockRunUtil);
     }
 
-    /* Verifies that setUp will throw exception when there is no vbmeta.img in the zip file*/
+    /* Verifies that setUp can pass when there is no vbmeta.img is provided*/
     @Test
-    public void testSetUp_NoVbmetaImageInGsiZip() throws Exception {
+    public void testSetUp_Success_NoVbmetaImage() throws Exception {
         File gsiDir = FileUtil.createTempDir("gsi_folder", mTmpDir);
         File systemImg = new File(gsiDir, "system.img");
-        File gsiZip = FileUtil.createTempFile("gsi_image", ".zip", mTmpDir);
-        ZipUtil.createZip(List.of(systemImg), gsiZip);
-        mBuildInfo.setFile("gsi_system.img", gsiZip, "0");
+        FileUtil.writeToFile("ddd", systemImg);
+        mBuildInfo.setFile("gsi_system.img", systemImg, "0");
+        mMockDevice.waitForDeviceOnline();
+        EasyMock.expect(mMockDevice.getApiLevel()).andReturn(29);
+        mMockDevice.rebootIntoBootloader();
+        mMockRunUtil.allowInterrupt(false);
+        mMockDevice.rebootIntoFastbootd();
+        doGetSlotExpectation();
+        EasyMock.expect(
+                        mMockDevice.executeLongFastbootCommand(
+                                "delete-logical-partition", "product_a"))
+                .andReturn(mSuccessResult);
+        EasyMock.expect(mMockDevice.executeLongFastbootCommand("erase", "system_a"))
+                .andReturn(mSuccessResult);
+        EasyMock.expect(
+                        mMockDevice.executeLongFastbootCommand(
+                                "flash",
+                                "system",
+                                mBuildInfo.getFile("gsi_system.img").getAbsolutePath()))
+                .andReturn(mSuccessResult);
+        EasyMock.expect(mMockDevice.executeLongFastbootCommand("-w")).andReturn(mSuccessResult);
+        mMockRunUtil.allowInterrupt(true);
+        doSetupExpectations();
         EasyMock.replay(mMockDevice, mMockRunUtil);
-        try {
-            mPreparer.setUp(mTestInfo);
-            fail("TargetSetupError is expected");
-        } catch (TargetSetupError e) {
-            // expected
-        }
+        mPreparer.setUp(mTestInfo);
         EasyMock.verify(mMockDevice, mMockRunUtil);
     }
 
diff --git a/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
index 334ee6e..0652a21 100644
--- a/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
@@ -24,6 +24,7 @@
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.command.remote.DeviceDescriptor;
 import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.ITestDevice.ApexInfo;
 import com.android.tradefed.invoker.IInvocationContext;
@@ -35,6 +36,7 @@
 
 import com.google.common.collect.ImmutableSet;
 
+import java.util.Arrays;
 import org.easymock.EasyMock;
 import org.junit.After;
 import org.junit.Before;
@@ -60,6 +62,8 @@
     private TestInformation mTestInfo;
     private BundletoolUtil mMockBundletoolUtil;
     private File mFakeApex;
+    private File mFakeApex2;
+    private File mFakeApex3;
     private File mFakeApk;
     private File mFakeApk2;
     private File mFakePersistentApk;
@@ -68,6 +72,8 @@
     private File mBundletoolJar;
     private OptionSetter mSetter;
     private static final String APEX_PACKAGE_NAME = "com.android.FAKE_APEX_PACKAGE_NAME";
+    private static final String APEX2_PACKAGE_NAME = "com.android.FAKE_APEX2_PACKAGE_NAME";
+    private static final String APEX3_PACKAGE_NAME = "com.android.FAKE_APEX3_PACKAGE_NAME";
     private static final String APK_PACKAGE_NAME = "com.android.FAKE_APK_PACKAGE_NAME";
     private static final String APK2_PACKAGE_NAME = "com.android.FAKE_APK2_PACKAGE_NAME";
     private static final String PERSISTENT_APK_PACKAGE_NAME = "com.android.PERSISTENT_PACKAGE_NAME";
@@ -78,6 +84,7 @@
     private static final String APEX_PACKAGE_KEYWORD = "FAKE_APEX_PACKAGE_NAME";
     private static final long APEX_VERSION = 1;
     private static final String APEX_NAME = "fakeApex.apex";
+    private static final String APEX2_NAME = "fakeApex_2.apex";
     private static final String APK_NAME = "fakeApk.apk";
     private static final String APK2_NAME = "fakeSecondApk.apk";
     private static final String PERSISTENT_APK_NAME = "fakePersistentApk.apk";
@@ -92,6 +99,8 @@
     @Before
     public void setUp() throws Exception {
         mFakeApex = FileUtil.createTempFile("fakeApex", ".apex");
+        mFakeApex2 = FileUtil.createTempFile("fakeApex_2", ".apex");
+        mFakeApex3 = FileUtil.createTempFile("fakeApex_3", ".apex");
         mFakeApk = FileUtil.createTempFile("fakeApk", ".apk");
         mFakeApk2 = FileUtil.createTempFile("fakeSecondApk", ".apk");
         mFakePersistentApk = FileUtil.createTempFile("fakePersistentApk", ".apk");
@@ -127,6 +136,12 @@
                     protected File getLocalPathForFilename(
                             TestInformation testInfo, String appFileName) throws TargetSetupError {
                         if (appFileName.endsWith(".apex")) {
+                            if (appFileName.contains("fakeApex_2")) {
+                                return mFakeApex2;
+                            }
+                            else if (appFileName.contains("fakeApex_3")) {
+                                return mFakeApex3;
+                            }
                             return mFakeApex;
                         }
                         if (appFileName.endsWith(".apk")) {
@@ -138,13 +153,11 @@
                                 return mFakeApk;
                             }
                         }
-                        if (appFileName.endsWith(".apks")) {
-                            if (appFileName.contains("Apex")) {
-                                return mFakeApexApks;
-                            }
-                            if (appFileName.contains("Apk")) {
-                                return mFakeApkApks;
-                            }
+                        if (SPLIT_APEX_APKS_NAME.equals(appFileName)) {
+                            return mFakeApexApks;
+                        }
+                        if (SPLIT_APK__APKS_NAME.equals(appFileName)) {
+                            return mFakeApkApks;
                         }
                         if (appFileName.endsWith(".jar")) {
                             return mBundletoolJar;
@@ -156,6 +169,12 @@
                     protected String parsePackageName(
                             File testAppFile, DeviceDescriptor deviceDescriptor) {
                         if (testAppFile.getName().endsWith(".apex")) {
+                            if (testAppFile.getName().contains("fakeApex_2")) {
+                                return APEX2_PACKAGE_NAME;
+                            }
+                            else if (testAppFile.getName().contains("fakeApex_3")) {
+                                return APEX3_PACKAGE_NAME;
+                            }
                             return APEX_PACKAGE_NAME;
                         }
                         if (testAppFile.getName().endsWith(".apk")
@@ -185,6 +204,8 @@
                         ApexInfo apexInfo;
                         if (apex.getName().contains("Split")) {
                             apexInfo = new ApexInfo(SPLIT_APEX_PACKAGE_NAME, APEX_VERSION);
+                        } else if (apex.getName().contains("fakeApex_2")) {
+                            apexInfo = new ApexInfo(APEX2_PACKAGE_NAME, APEX_VERSION);
                         } else {
                             apexInfo = new ApexInfo(APEX_PACKAGE_NAME, APEX_VERSION);
                         }
@@ -209,11 +230,343 @@
     @After
     public void tearDown() throws Exception {
         FileUtil.deleteFile(mFakeApex);
+        FileUtil.deleteFile(mFakeApex2);
+        FileUtil.deleteFile(mFakeApex3);
         FileUtil.deleteFile(mFakeApk);
         FileUtil.deleteFile(mFakeApk2);
         FileUtil.deleteFile(mFakePersistentApk);
     }
 
+    /**
+     * Test that it gets the correct apex files that are already installed on the /data directory.
+     */
+    @Test
+    public void testGetApexInData() throws Exception {
+        Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
+        Set<ApexInfo> expectedApex = new HashSet<ApexInfo>();
+
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexData2 =
+                new ApexInfo(
+                        APEX2_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX2_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexSystem =
+                new ApexInfo(
+                        "com.android.FAKE_APEX3_PACKAGE_NAME",
+                        1,
+                        "/system/apex/com.android.FAKE_APEX3_PACKAGE_NAME@1.apex");
+
+        activatedApex = new HashSet<>(Arrays.asList(fakeApexData, fakeApexData2, fakeApexSystem));
+        expectedApex = new HashSet<>(Arrays.asList(fakeApexData, fakeApexData2));
+        assertEquals(2, mInstallApexModuleTargetPreparer.getApexInData(activatedApex).size());
+        assertEquals(expectedApex, mInstallApexModuleTargetPreparer.getApexInData(activatedApex));
+
+        activatedApex = new HashSet<>(Arrays.asList(fakeApexSystem));
+        assertEquals(0, mInstallApexModuleTargetPreparer.getApexInData(activatedApex).size());
+    }
+
+    /**
+     * Test that it returns the correct files to be installed and uninstalled.
+     */
+    @Test
+    public void testGetModulesToUninstall_NoneUninstallAndInstallFiles() throws Exception {
+        Set<ApexInfo> apexInData = new HashSet<>();
+        List<File> testFiles = new ArrayList<>();
+        testFiles.add(mFakeApex);
+        testFiles.add(mFakeApex2);
+
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexData2 =
+                new ApexInfo(
+                        APEX2_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX2_PACKAGE_NAME@1.apex");
+
+        apexInData.add(fakeApexData);
+        apexInData.add(fakeApexData2);
+
+        EasyMock.replay(mMockBuildInfo, mMockDevice);
+        Set<ApexInfo> results = mInstallApexModuleTargetPreparer.getModulesToUninstall(
+                apexInData, testFiles, mMockDevice);
+
+        assertEquals(0, testFiles.size());
+        assertEquals(0, results.size());
+        EasyMock.verify(mMockBuildInfo, mMockDevice);
+    }
+
+    /**
+     * Test that it returns the correct files to be installed and uninstalled.
+     */
+    @Test
+    public void testGetModulesToUninstall_UninstallAndInstallFiles() throws Exception {
+        Set<ApexInfo> apexInData = new HashSet<>();
+        List<File> testFiles = new ArrayList<>();
+        testFiles.add(mFakeApex3);
+
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexData2 =
+                new ApexInfo(
+                        APEX2_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX2_PACKAGE_NAME@1.apex");
+
+        apexInData.add(fakeApexData);
+        apexInData.add(fakeApexData2);
+
+        EasyMock.replay(mMockBuildInfo, mMockDevice);
+        Set<ApexInfo> results = mInstallApexModuleTargetPreparer.getModulesToUninstall(
+                apexInData, testFiles, mMockDevice);
+        assertEquals(1, testFiles.size());
+        assertEquals(mFakeApex3, testFiles.get(0));
+        assertEquals(2, results.size());
+        results.containsAll(apexInData);
+        EasyMock.verify(mMockBuildInfo, mMockDevice);
+    }
+
+    /**
+     * Test that it returns the correct files to be installed and uninstalled.
+     */
+    @Test
+    public void testGetModulesToUninstall_UninstallAndInstallFiles2() throws Exception {
+        Set<ApexInfo> apexInData = new HashSet<>();
+        List<File> testFiles = new ArrayList<>();
+        testFiles.add(mFakeApex2);
+        testFiles.add(mFakeApex3);
+
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexData2 =
+                new ApexInfo(
+                        APEX2_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX2_PACKAGE_NAME@1.apex");
+
+        apexInData.add(fakeApexData);
+        apexInData.add(fakeApexData2);
+
+        EasyMock.replay(mMockDevice);
+        Set<ApexInfo> results =
+                mInstallApexModuleTargetPreparer.getModulesToUninstall(
+                        apexInData, testFiles, mMockDevice);
+        assertEquals(1, testFiles.size());
+        assertEquals(mFakeApex3, testFiles.get(0));
+        assertEquals(1, results.size());
+        results.contains(fakeApexData);
+        EasyMock.verify(mMockDevice);
+    }
+
+    /**
+     * Test the method behaves the same process when the files to be installed contain apk or apks.
+     */
+    @Test
+    public void testSetupAndTearDown_Optimize_APEXANDAPK_Reboot() throws Exception {
+        mSetter.setOptionValue("skip-apex-teardown", "true");
+        mInstallApexModuleTargetPreparer.addTestFileName(APEX_NAME);
+        mInstallApexModuleTargetPreparer.addTestFileName(APK_NAME);
+
+        mMockDevice.deleteFile(APEX_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(1);
+        mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(1);
+        mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(1);
+        CommandResult res = new CommandResult();
+        res.setStdout("test.apex");
+        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);
+        mMockDevice.reboot();
+        EasyMock.expectLastCall();
+
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(new HashSet<>()).times(2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(
+                new HashSet<>(Arrays.asList(fakeApexData))).atLeastOnce();
+        mockSuccessfulInstallMultiPackageAndReboot();
+        Set<String> installableModules = new HashSet<>();
+        installableModules.add(APK_PACKAGE_NAME);
+        installableModules.add(APEX_PACKAGE_NAME);
+        EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+        EasyMock.replay(mMockDevice);
+        mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+        EasyMock.verify(mMockDevice);
+    }
+
+    /**
+     * Test the method will optimize the process and it will not reboot because the files to be
+     * installed are already installed on the device.
+     */
+    @Test
+    public void testSetupAndTearDown_Optimize_MultipleAPEX_NoReboot() throws Exception {
+        mSetter.setOptionValue("skip-apex-teardown", "true");
+        mInstallApexModuleTargetPreparer.addTestFileName(APEX_NAME);
+        mInstallApexModuleTargetPreparer.addTestFileName(APEX2_NAME);
+
+        Set<ApexInfo> apexInData = new HashSet<>();
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexData2 =
+                new ApexInfo(
+                        APEX2_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX2_PACKAGE_NAME@1.apex");
+
+        apexInData.add(fakeApexData);
+        apexInData.add(fakeApexData2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(apexInData).times(2);
+        Set<String> installableModules = new HashSet<>();
+        installableModules.add(APEX_PACKAGE_NAME);
+        installableModules.add(APEX2_PACKAGE_NAME);
+        EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+        EasyMock.replay(mMockDevice);
+        mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+        EasyMock.verify(mMockDevice);
+    }
+
+    /**
+     * Test the method will uninstall the unused files and install the required files for the
+     * current test, and finally reboot the device.
+     */
+    @Test
+    public void testSetupAndTearDown_Optimize_MultipleAPEX_UninstallThenInstallAndReboot()
+            throws Exception {
+        mSetter.setOptionValue("skip-apex-teardown", "true");
+        mInstallApexModuleTargetPreparer.addTestFileName(APEX2_NAME);
+
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexData2 =
+                new ApexInfo(
+                        APEX2_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX2_PACKAGE_NAME@1.apex");
+
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(
+                new HashSet<>(Arrays.asList(fakeApexData))).times(2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(
+                new HashSet<>(Arrays.asList(fakeApexData2))).atLeastOnce();
+        Set<String> installableModules = new HashSet<>();
+        installableModules.add(APEX2_PACKAGE_NAME);
+        EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+        EasyMock.expect(mMockDevice.uninstallPackage(EasyMock.anyObject()))
+                .andReturn(null)
+                .once();
+        mockSuccessfulInstallPackageAndReboot(mFakeApex2);
+        EasyMock.replay(mMockDevice);
+        mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+        mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
+        EasyMock.verify(mMockDevice);
+    }
+
+    /**
+     * Test the method will uninstall the unused files for the current test, and finally reboot the
+     * device.
+     */
+    @Test
+    public void testSetupAndTearDown_Optimize_MultipleAPEX_UninstallAndReboot() throws Exception {
+        mSetter.setOptionValue("skip-apex-teardown", "true");
+        mInstallApexModuleTargetPreparer.addTestFileName(APEX2_NAME);
+
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexData2 =
+                new ApexInfo(
+                        APEX2_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX2_PACKAGE_NAME@1.apex");
+
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(
+                new HashSet<>(Arrays.asList(fakeApexData, fakeApexData2))).times(2);
+        Set<String> installableModules = new HashSet<>();
+        installableModules.add(APEX2_PACKAGE_NAME);
+        EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+        EasyMock.expect(mMockDevice.uninstallPackage(EasyMock.anyObject()))
+                .andReturn(null)
+                .once();
+        mMockDevice.reboot();
+        EasyMock.expectLastCall().once();
+        EasyMock.replay(mMockDevice);
+        mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+        mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
+        EasyMock.verify(mMockDevice);
+    }
+
+    /**
+     * Test the method will install the required files for the current test, and finally reboot the
+     * device.
+     */
+    @Test
+    public void testSetupAndTearDown_Optimize_MultipleAPEX_Reboot() throws Exception {
+        mSetter.setOptionValue("skip-apex-teardown", "true");
+        mInstallApexModuleTargetPreparer.addTestFileName(APEX_NAME);
+        mInstallApexModuleTargetPreparer.addTestFileName(APEX2_NAME);
+
+        Set<ApexInfo> apexInData = new HashSet<>();
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexData2 =
+                new ApexInfo(
+                        APEX2_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX2_PACKAGE_NAME@1.apex");
+
+        apexInData.add(fakeApexData);
+        apexInData.add(fakeApexData2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(
+                new HashSet<>(Arrays.asList(fakeApexData))).times(2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(apexInData).atLeastOnce();
+        Set<String> installableModules = new HashSet<>();
+        installableModules.add(APEX_PACKAGE_NAME);
+        installableModules.add(APEX2_PACKAGE_NAME);
+        EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+        mockSuccessfulInstallPackageAndReboot(mFakeApex2);
+        EasyMock.replay(mMockDevice);
+        mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+        mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
+        EasyMock.verify(mMockDevice);
+    }
+
     @Test
     public void testSetupSuccess_removeExistingStagedApexSuccess() throws Exception {
         mInstallApexModuleTargetPreparer.addTestFileName(APEX_NAME);
@@ -304,6 +657,39 @@
         EasyMock.verify(mMockBuildInfo, mMockDevice);
     }
 
+    @Test
+    public void testSetupSuccess_withAbsoluteTestFileName() throws Exception {
+        mInstallApexModuleTargetPreparer.addTestFile(mFakeApex);
+        mMockDevice.deleteFile(APEX_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(1);
+        mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(1);
+        mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(1);
+        CommandResult res = new CommandResult();
+        res.setStdout("test.apex");
+        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);
+        mMockDevice.reboot();
+        EasyMock.expectLastCall();
+        mockSuccessfulInstallPackageAndReboot(mFakeApex);
+        Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
+        activatedApex.add(
+                new ApexInfo(
+                        "com.android.FAKE_APEX_PACKAGE_NAME",
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex"));
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(3);
+        Set<String> installableModules = new HashSet<>();
+        installableModules.add(APEX_PACKAGE_NAME);
+        EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+
+        EasyMock.replay(mMockBuildInfo, mMockDevice);
+        mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+        EasyMock.verify(mMockBuildInfo, mMockDevice);
+    }
+
     @Test(expected = TargetSetupError.class)
     public void testSetupFail_getActivatedPackageSuccessThrowModuleNotPreloaded() throws Exception {
         mInstallApexModuleTargetPreparer.addTestFileName(APK_NAME);
@@ -748,19 +1134,7 @@
     @Test
     public void testSetupAndTearDown() throws Exception {
         mInstallApexModuleTargetPreparer.addTestFileName(APEX_NAME);
-        mMockDevice.deleteFile(APEX_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        CommandResult res = new CommandResult();
-        res.setStdout("test.apex");
-        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);
-        mMockDevice.reboot();
-        EasyMock.expectLastCall();
+        mockCleanInstalledApexPackagesAndReboot();
         mockSuccessfulInstallPackageAndReboot(mFakeApex);
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(
@@ -801,19 +1175,7 @@
     public void testSetupAndTearDown_MultiInstall() throws Exception {
         mInstallApexModuleTargetPreparer.addTestFileName(APEX_NAME);
         mInstallApexModuleTargetPreparer.addTestFileName(APK_NAME);
-        mMockDevice.deleteFile(APEX_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        CommandResult res = new CommandResult();
-        res.setStdout("test.apex");
-        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);
-        mMockDevice.reboot();
-        EasyMock.expectLastCall();
+        mockCleanInstalledApexPackagesAndReboot();
         mockSuccessfulInstallMultiPackageAndReboot();
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(
@@ -858,22 +1220,7 @@
         mBundletoolJar = File.createTempFile("bundletool", ".jar");
         File splitApk2 = File.createTempFile("fakeSplitApk2", ".apk", fakeSplitApkApks);
         try {
-            mMockDevice.deleteFile(APEX_DATA_DIR + "*");
-            EasyMock.expectLastCall().times(2);
-            mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
-            EasyMock.expectLastCall().times(2);
-            mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
-            EasyMock.expectLastCall().times(2);
-            CommandResult res = new CommandResult();
-            res.setStdout("test.apex");
-            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);
-            mMockDevice.reboot();
-            EasyMock.expectLastCall();
+            mockCleanInstalledApexPackagesAndReboot();
             when(mMockBundletoolUtil.generateDeviceSpecFile(Mockito.any(ITestDevice.class)))
                     .thenReturn("serial.json");
 
@@ -965,6 +1312,340 @@
     }
 
     @Test
+    public void testInstallUsingBundletool_AbsolutePath() throws Exception {
+        mInstallApexModuleTargetPreparer.addTestFileName(SPLIT_APEX_APKS_NAME);
+        mInstallApexModuleTargetPreparer.addTestFileName(SPLIT_APK__APKS_NAME);
+        mFakeApexApks = File.createTempFile("fakeApex", ".apks");
+        mFakeApkApks = File.createTempFile("fakeApk", ".apks");
+
+        File fakeSplitApexApks = File.createTempFile("ApexSplits", "");
+        fakeSplitApexApks.delete();
+        fakeSplitApexApks.mkdir();
+        File splitApex = File.createTempFile("fakeSplitApex", ".apex", fakeSplitApexApks);
+
+        File fakeSplitApkApks = File.createTempFile("ApkSplits", "");
+        fakeSplitApkApks.delete();
+        fakeSplitApkApks.mkdir();
+        File splitApk1 = File.createTempFile("fakeSplitApk1", ".apk", fakeSplitApkApks);
+        mBundletoolJar = File.createTempFile("/fake/absolute/path/bundletool", ".jar");
+        File splitApk2 = File.createTempFile("fakeSplitApk2", ".apk", fakeSplitApkApks);
+        try {
+            mockCleanInstalledApexPackagesAndReboot();
+            when(mMockBundletoolUtil.generateDeviceSpecFile(Mockito.any(ITestDevice.class)))
+                    .thenReturn("serial.json");
+
+            assertTrue(fakeSplitApexApks != null);
+            assertTrue(fakeSplitApkApks != null);
+            assertTrue(mFakeApexApks != null);
+            assertTrue(mFakeApkApks != null);
+            assertEquals(1, fakeSplitApexApks.listFiles().length);
+            assertEquals(2, fakeSplitApkApks.listFiles().length);
+
+            when(mMockBundletoolUtil.extractSplitsFromApks(
+                            Mockito.eq(mFakeApexApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class)))
+                    .thenReturn(fakeSplitApexApks);
+
+            when(mMockBundletoolUtil.extractSplitsFromApks(
+                            Mockito.eq(mFakeApkApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class)))
+                    .thenReturn(fakeSplitApkApks);
+
+            mMockDevice.waitForDeviceAvailable();
+
+            List<String> trainInstallCmd = new ArrayList<>();
+            trainInstallCmd.add("install-multi-package");
+            trainInstallCmd.add(splitApex.getAbsolutePath());
+            String cmd = "";
+            for (File f : fakeSplitApkApks.listFiles()) {
+                if (!cmd.isEmpty()) {
+                    cmd += ":" + f.getParentFile().getAbsolutePath() + "/" + f.getName();
+                } else {
+                    cmd += f.getParentFile().getAbsolutePath() + "/" + f.getName();
+                }
+            }
+            trainInstallCmd.add(cmd);
+            EasyMock.expect(mMockDevice.executeAdbCommand(trainInstallCmd.toArray(new String[0])))
+                    .andReturn("Success")
+                    .once();
+            mMockDevice.reboot();
+            Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
+            activatedApex.add(
+                    new ApexInfo(
+                            SPLIT_APEX_PACKAGE_NAME,
+                            1,
+                            "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex"));
+            EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(3);
+            EasyMock.expect(mMockDevice.uninstallPackage(SPLIT_APK_PACKAGE_NAME))
+                    .andReturn(null)
+                    .once();
+            mMockDevice.reboot();
+            EasyMock.expectLastCall();
+            Set<String> installableModules = new HashSet<>();
+            installableModules.add(APEX_PACKAGE_NAME);
+            installableModules.add(SPLIT_APK_PACKAGE_NAME);
+            EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+
+            EasyMock.replay(mMockBuildInfo, mMockDevice);
+            mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+            mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
+            Mockito.verify(mMockBundletoolUtil, times(1))
+                    .generateDeviceSpecFile(Mockito.any(ITestDevice.class));
+            // Extract splits 1 time to get the package name for the module, and again during
+            // installation.
+            Mockito.verify(mMockBundletoolUtil, times(2))
+                    .extractSplitsFromApks(
+                            Mockito.eq(mFakeApexApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class));
+            Mockito.verify(mMockBundletoolUtil, times(2))
+                    .extractSplitsFromApks(
+                            Mockito.eq(mFakeApkApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class));
+            EasyMock.verify(mMockBuildInfo, mMockDevice);
+        } finally {
+            FileUtil.deleteFile(mFakeApexApks);
+            FileUtil.deleteFile(mFakeApkApks);
+            FileUtil.recursiveDelete(fakeSplitApexApks);
+            FileUtil.deleteFile(fakeSplitApexApks);
+            FileUtil.recursiveDelete(fakeSplitApkApks);
+            FileUtil.deleteFile(fakeSplitApkApks);
+            FileUtil.deleteFile(mBundletoolJar);
+        }
+    }
+
+    @Test
+    public void testInstallUsingBundletool_TrainFolder() throws Exception {
+        File trainFolder = File.createTempFile("tmpTrain", "");
+        trainFolder.delete();
+        trainFolder.mkdir();
+        mSetter.setOptionValue("train-path", trainFolder.getAbsolutePath());
+        mFakeApexApks = File.createTempFile("fakeApex", ".apks", trainFolder);
+        mFakeApkApks = File.createTempFile("fakeApk", ".apks", trainFolder);
+
+        File fakeSplitApexApks = File.createTempFile("ApexSplits", "");
+        fakeSplitApexApks.delete();
+        fakeSplitApexApks.mkdir();
+        File splitApex = File.createTempFile("fakeSplitApex", ".apex", fakeSplitApexApks);
+
+        File fakeSplitApkApks = File.createTempFile("ApkSplits", "");
+        fakeSplitApkApks.delete();
+        fakeSplitApkApks.mkdir();
+        File splitApk1 = File.createTempFile("fakeSplitApk1", ".apk", fakeSplitApkApks);
+        mBundletoolJar = File.createTempFile("bundletool", ".jar");
+        File splitApk2 = File.createTempFile("fakeSplitApk2", ".apk", fakeSplitApkApks);
+        try {
+            mockCleanInstalledApexPackagesAndReboot();
+            when(mMockBundletoolUtil.generateDeviceSpecFile(Mockito.any(ITestDevice.class)))
+                    .thenReturn("serial.json");
+
+            assertTrue(fakeSplitApexApks != null);
+            assertTrue(fakeSplitApkApks != null);
+            assertTrue(mFakeApexApks != null);
+            assertTrue(mFakeApkApks != null);
+            assertEquals(1, fakeSplitApexApks.listFiles().length);
+            assertEquals(2, fakeSplitApkApks.listFiles().length);
+
+            when(mMockBundletoolUtil.extractSplitsFromApks(
+                            Mockito.eq(mFakeApexApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class)))
+                    .thenReturn(fakeSplitApexApks);
+
+            when(mMockBundletoolUtil.extractSplitsFromApks(
+                            Mockito.eq(mFakeApkApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class)))
+                    .thenReturn(fakeSplitApkApks);
+
+            mMockDevice.waitForDeviceAvailable();
+
+            List<String> trainInstallCmd = new ArrayList<>();
+            trainInstallCmd.add("install-multi-package");
+            trainInstallCmd.add(splitApex.getAbsolutePath());
+            String cmd = "";
+            for (File f : fakeSplitApkApks.listFiles()) {
+                if (!cmd.isEmpty()) {
+                    cmd += ":" + f.getParentFile().getAbsolutePath() + "/" + f.getName();
+                } else {
+                    cmd += f.getParentFile().getAbsolutePath() + "/" + f.getName();
+                }
+            }
+            trainInstallCmd.add(cmd);
+            EasyMock.expect(mMockDevice.executeAdbCommand(trainInstallCmd.toArray(new String[0])))
+                    .andReturn("Success")
+                    .once();
+            mMockDevice.reboot();
+            Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
+            activatedApex.add(
+                    new ApexInfo(
+                            SPLIT_APEX_PACKAGE_NAME,
+                            1,
+                            "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex"));
+            EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(3);
+            EasyMock.expect(mMockDevice.uninstallPackage(SPLIT_APK_PACKAGE_NAME))
+                    .andReturn(null)
+                    .once();
+            mMockDevice.reboot();
+            EasyMock.expectLastCall();
+            Set<String> installableModules = new HashSet<>();
+            installableModules.add(APEX_PACKAGE_NAME);
+            installableModules.add(SPLIT_APK_PACKAGE_NAME);
+            EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+
+            EasyMock.replay(mMockBuildInfo, mMockDevice);
+            mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+            mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
+            Mockito.verify(mMockBundletoolUtil, times(1))
+                    .generateDeviceSpecFile(Mockito.any(ITestDevice.class));
+            // Extract splits 1 time to get the package name for the module, and again during
+            // installation.
+            Mockito.verify(mMockBundletoolUtil, times(2))
+                    .extractSplitsFromApks(
+                            Mockito.eq(mFakeApexApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class));
+            Mockito.verify(mMockBundletoolUtil, times(2))
+                    .extractSplitsFromApks(
+                            Mockito.eq(mFakeApkApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class));
+            EasyMock.verify(mMockBuildInfo, mMockDevice);
+        } finally {
+            FileUtil.recursiveDelete(trainFolder);
+            FileUtil.deleteFile(trainFolder);
+            FileUtil.deleteFile(mFakeApexApks);
+            FileUtil.deleteFile(mFakeApkApks);
+            FileUtil.recursiveDelete(fakeSplitApexApks);
+            FileUtil.deleteFile(fakeSplitApexApks);
+            FileUtil.recursiveDelete(fakeSplitApkApks);
+            FileUtil.deleteFile(fakeSplitApkApks);
+            FileUtil.deleteFile(mBundletoolJar);
+        }
+    }
+
+    @Test
+    public void testInstallUsingBundletool_AllFilesHaveAbsolutePath() throws Exception {
+        mFakeApexApks = File.createTempFile("fakeApex", ".apks");
+        mFakeApkApks = File.createTempFile("fakeApk", ".apks");
+        mInstallApexModuleTargetPreparer.addTestFile(mFakeApexApks);
+        mInstallApexModuleTargetPreparer.addTestFile(mFakeApkApks);
+
+        File fakeSplitApexApks = File.createTempFile("ApexSplits", "");
+        fakeSplitApexApks.delete();
+        fakeSplitApexApks.mkdir();
+        File splitApex = File.createTempFile("fakeSplitApex", ".apex", fakeSplitApexApks);
+
+        File fakeSplitApkApks = File.createTempFile("ApkSplits", "");
+        fakeSplitApkApks.delete();
+        fakeSplitApkApks.mkdir();
+        File splitApk1 = File.createTempFile("fakeSplitApk1", ".apk", fakeSplitApkApks);
+        mBundletoolJar = File.createTempFile("/fake/absolute/path/bundletool", ".jar");
+        File splitApk2 = File.createTempFile("fakeSplitApk2", ".apk", fakeSplitApkApks);
+        try {
+            mockCleanInstalledApexPackagesAndReboot();
+            when(mMockBundletoolUtil.generateDeviceSpecFile(Mockito.any(ITestDevice.class)))
+                    .thenReturn("serial.json");
+
+            assertTrue(fakeSplitApexApks != null);
+            assertTrue(fakeSplitApkApks != null);
+            assertTrue(mFakeApexApks != null);
+            assertTrue(mFakeApkApks != null);
+            assertEquals(1, fakeSplitApexApks.listFiles().length);
+            assertEquals(2, fakeSplitApkApks.listFiles().length);
+
+            when(mMockBundletoolUtil.extractSplitsFromApks(
+                            Mockito.eq(mFakeApexApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class)))
+                    .thenReturn(fakeSplitApexApks);
+
+            when(mMockBundletoolUtil.extractSplitsFromApks(
+                            Mockito.eq(mFakeApkApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class)))
+                    .thenReturn(fakeSplitApkApks);
+
+            mMockDevice.waitForDeviceAvailable();
+
+            List<String> trainInstallCmd = new ArrayList<>();
+            trainInstallCmd.add("install-multi-package");
+            trainInstallCmd.add(splitApex.getAbsolutePath());
+            String cmd = "";
+            for (File f : fakeSplitApkApks.listFiles()) {
+                if (!cmd.isEmpty()) {
+                    cmd += ":" + f.getParentFile().getAbsolutePath() + "/" + f.getName();
+                } else {
+                    cmd += f.getParentFile().getAbsolutePath() + "/" + f.getName();
+                }
+            }
+            trainInstallCmd.add(cmd);
+            EasyMock.expect(mMockDevice.executeAdbCommand(trainInstallCmd.toArray(new String[0])))
+                    .andReturn("Success")
+                    .once();
+            mMockDevice.reboot();
+            Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
+            activatedApex.add(
+                    new ApexInfo(
+                            SPLIT_APEX_PACKAGE_NAME,
+                            1,
+                            "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex"));
+            EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(3);
+            EasyMock.expect(mMockDevice.uninstallPackage(SPLIT_APK_PACKAGE_NAME))
+                    .andReturn(null)
+                    .once();
+            mMockDevice.reboot();
+            EasyMock.expectLastCall();
+            Set<String> installableModules = new HashSet<>();
+            installableModules.add(APEX_PACKAGE_NAME);
+            installableModules.add(SPLIT_APK_PACKAGE_NAME);
+            EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+
+            EasyMock.replay(mMockBuildInfo, mMockDevice);
+            mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+            mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
+            Mockito.verify(mMockBundletoolUtil, times(1))
+                    .generateDeviceSpecFile(Mockito.any(ITestDevice.class));
+            // Extract splits 1 time to get the package name for the module, and again during
+            // installation.
+            Mockito.verify(mMockBundletoolUtil, times(2))
+                    .extractSplitsFromApks(
+                            Mockito.eq(mFakeApexApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class));
+            Mockito.verify(mMockBundletoolUtil, times(2))
+                    .extractSplitsFromApks(
+                            Mockito.eq(mFakeApkApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class));
+            EasyMock.verify(mMockBuildInfo, mMockDevice);
+        } finally {
+            FileUtil.deleteFile(mFakeApexApks);
+            FileUtil.deleteFile(mFakeApkApks);
+            FileUtil.recursiveDelete(fakeSplitApexApks);
+            FileUtil.deleteFile(fakeSplitApexApks);
+            FileUtil.recursiveDelete(fakeSplitApkApks);
+            FileUtil.deleteFile(fakeSplitApkApks);
+            FileUtil.deleteFile(mBundletoolJar);
+        }
+    }
+
+    @Test
     public void testInstallUsingBundletool_skipModuleNotPreloaded() throws Exception {
         mSetter.setOptionValue("ignore-if-module-not-preloaded", "true");
         mInstallApexModuleTargetPreparer.addTestFileName(SPLIT_APEX_APKS_NAME);
@@ -1036,6 +1717,7 @@
                     .once();
             Set<String> installableModules = new HashSet<>();
             installableModules.add(SPLIT_APK_PACKAGE_NAME);
+
             EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
 
             EasyMock.replay(mMockBuildInfo, mMockDevice);
@@ -1115,6 +1797,22 @@
         EasyMock.expectLastCall().once();
     }
 
+    private void mockCleanInstalledApexPackagesAndReboot() throws DeviceNotAvailableException {
+        mMockDevice.deleteFile(APEX_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(2);
+        mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(2);
+        mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(2);
+        CommandResult res = new CommandResult();
+        res.setStdout("test.apex");
+        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);
+        mMockDevice.reboot();
+        EasyMock.expectLastCall();
+    }
+
     @Test
     public void testSetupAndTearDown_noModulesPreloaded() throws Exception {
         mSetter.setOptionValue("ignore-if-module-not-preloaded", "true");
@@ -1150,19 +1848,7 @@
         mInstallApexModuleTargetPreparer.addTestFileName(APK_NAME);
         // Module not preloaded.
         mInstallApexModuleTargetPreparer.addTestFileName(APK2_NAME);
-        mMockDevice.deleteFile(APEX_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        CommandResult res = new CommandResult();
-        res.setStdout("test.apex");
-        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);
-        mMockDevice.reboot();
-        EasyMock.expectLastCall();
+        mockCleanInstalledApexPackagesAndReboot();
         mockSuccessfulInstallMultiPackageAndReboot();
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(
@@ -1192,19 +1878,7 @@
         mInstallApexModuleTargetPreparer.addTestFileName(APEX_NAME);
         mInstallApexModuleTargetPreparer.addTestFileName(APK_NAME);
         mInstallApexModuleTargetPreparer.addTestFileName(APK2_NAME);
-        mMockDevice.deleteFile(APEX_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        CommandResult res = new CommandResult();
-        res.setStdout("test.apex");
-        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);
-        mMockDevice.reboot();
-        EasyMock.expectLastCall();
+        mockCleanInstalledApexPackagesAndReboot();
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(
                 new ApexInfo(
diff --git a/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java b/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
index 16ec66f..853803c 100644
--- a/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
@@ -42,6 +42,7 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.io.IOException;
 import java.io.File;
 import java.util.Set;
 
@@ -645,6 +646,53 @@
         }
     }
 
+    /**
+     * Test that if multiple files exists after delayed partial download, push the one with matching
+     * ABI.
+     */
+    @Test
+    public void testPush_moduleName_files_abi_delayedDownload() throws Exception {
+        mOptionSetter.setOptionValue("push", "file->/data/local/tmp/file");
+        mPreparer.setAbi(new Abi("x86", "32"));
+
+        mPreparer.setInvocationContext(createModuleWithName("aaaaa"));
+        File tmpFolder = FileUtil.createTempDir("push-file-tests-dir");
+        IDeviceBuildInfo info =
+                new DeviceBuildInfo() {
+                    @Override
+                    public File stageRemoteFile(String fileName, File workingDir) {
+                        try {
+                            File file_64 =
+                                    new File(tmpFolder, "target/testcases/aaaaa/x86_64/file");
+                            FileUtil.mkdirsRWX(file_64.getParentFile());
+                            file_64.createNewFile();
+                            File file_32 = new File(tmpFolder, "target/testcases/aaaaa/x86/file");
+                            FileUtil.mkdirsRWX(file_32.getParentFile());
+                            file_32.createNewFile();
+                            // Return the file with mismatched ABI.
+                            return file_64;
+                        } catch (IOException e) {
+                            return null;
+                        }
+                    }
+                };
+        try {
+            info.setFile(BuildInfoFileKey.TESTDIR_IMAGE, tmpFolder, "v1");
+            EasyMock.expect(
+                            mMockDevice.pushFile(
+                                    EasyMock.eq(
+                                            new File(tmpFolder, "target/testcases/aaaaa/x86/file")),
+                                    EasyMock.eq("/data/local/tmp/file")))
+                    .andReturn(true);
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
+            EasyMock.replay(mMockDevice);
+            mPreparer.setUp(mTestInfo);
+            EasyMock.verify(mMockDevice);
+        } finally {
+            FileUtil.recursiveDelete(tmpFolder);
+        }
+    }
+
     @Test
     public void testPush_moduleName_ignored() throws Exception {
         mOptionSetter.setOptionValue("push", "lib64->/data/local/tmp/lib");
diff --git a/tests/src/com/android/tradefed/targetprep/PythonVirtualenvPreparerTest.java b/tests/src/com/android/tradefed/targetprep/PythonVirtualenvPreparerTest.java
index 133bad5..20754fc 100644
--- a/tests/src/com/android/tradefed/targetprep/PythonVirtualenvPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/PythonVirtualenvPreparerTest.java
@@ -21,7 +21,10 @@
 import static org.easymock.EasyMock.createMock;
 import static org.easymock.EasyMock.createNiceMock;
 import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.eq;
 import static org.easymock.EasyMock.replay;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThat;
 
 import com.android.tradefed.build.BuildInfo;
 import com.android.tradefed.build.IBuildInfo;
@@ -30,6 +33,8 @@
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.IRunUtil;
 
+import com.google.common.base.Throwables;
+
 import junit.framework.TestCase;
 
 import java.io.File;
@@ -105,4 +110,44 @@
         mPreparer.installDeps(buildInfo, mMockDevice);
         assertTrue(buildInfo.getFile("PYTHONPATH") == null);
     }
+
+    public void testStartVirtualenv_throwTSE_whenVirtualenvNotFound() throws Exception {
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        result.setStdout("bash: virtualenv: command not found");
+        expect(mMockRunUtil.runTimedCmd(anyLong(), eq("virtualenv"), eq("--version")))
+                .andReturn(result);
+        replay(mMockRunUtil);
+
+        try {
+            mPreparer.startVirtualenv(new BuildInfo(), mMockDevice);
+            fail("startVirtualenv succeeded despite a failed command");
+        } catch (TargetSetupError e) {
+            assertThat(
+                    String.format(
+                            "An unexpected exception was thrown:\n%s",
+                            Throwables.getStackTraceAsString(e)),
+                    e.getMessage(),
+                    containsString("virtualenv is not installed."));
+        }
+    }
+
+    public void testStartVirtualenv_throwTSE_whenVirtualenvIsTooOld() throws Exception {
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        result.setStdout("virtualenv 16.7.10 from /path/to/site-packages/virtualenv/__init__.py");
+        expect(mMockRunUtil.runTimedCmd(anyLong(), eq("virtualenv"), eq("--version")))
+                .andReturn(result);
+        replay(mMockRunUtil);
+
+        try {
+            mPreparer.startVirtualenv(new BuildInfo(), mMockDevice);
+            fail("startVirtualenv succeeded despite a failed command");
+        } catch (TargetSetupError e) {
+            assertEquals(
+                    String.format(
+                            "An unexpected exception was thrown:\n%s",
+                            Throwables.getStackTraceAsString(e)),
+                    e.getMessage(),
+                    "virtualenv is too old. Required: >=20.0.1, yours: 16.7.10");
+        }
+    }
 }
\ No newline at end of file
diff --git a/tests/src/com/android/tradefed/targetprep/RunHostCommandTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/RunHostCommandTargetPreparerTest.java
index 3f7ae6d..a8f95cf 100644
--- a/tests/src/com/android/tradefed/targetprep/RunHostCommandTargetPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/RunHostCommandTargetPreparerTest.java
@@ -20,24 +20,29 @@
 import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.device.IDeviceManager;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.IRunUtil;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Answers;
+import org.mockito.InOrder;
 import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 import java.io.OutputStream;
 import java.util.Arrays;
@@ -45,21 +50,24 @@
 import java.util.List;
 
 /** Unit test for {@link RunHostCommandTargetPreparer}. */
-@RunWith(MockitoJUnitRunner.class)
+@RunWith(JUnit4.class)
 public final class RunHostCommandTargetPreparerTest {
 
     private static final String DEVICE_SERIAL = "123456";
     private static final String FULL_COMMAND = "command    \t\t\t  \t  argument $SERIAL";
 
-    @Mock private ITestDevice mDevice;
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private TestInformation mTestInfo;
+
     @Mock private RunHostCommandTargetPreparer.BgCommandLog mBgCommandLog;
     @Mock private IRunUtil mRunUtil;
+    @Mock private IDeviceManager mDeviceManager;
     private RunHostCommandTargetPreparer mPreparer;
-    private TestInformation mTestInfo;
 
     @Before
     public void setUp() {
-        when(mDevice.getSerialNumber()).thenReturn(DEVICE_SERIAL);
         mPreparer =
                 new RunHostCommandTargetPreparer() {
                     @Override
@@ -68,13 +76,16 @@
                     }
 
                     @Override
+                    IDeviceManager getDeviceManager() {
+                        return mDeviceManager;
+                    }
+
+                    @Override
                     protected List<BgCommandLog> createBgCommandLogs() {
                         return Collections.singletonList(mBgCommandLog);
                     }
                 };
-        IInvocationContext context = new InvocationContext();
-        context.addAllocatedDevice("device", mDevice);
-        mTestInfo = TestInformation.newBuilder().setInvocationContext(context).build();
+        when(mTestInfo.getDevice().getSerialNumber()).thenReturn(DEVICE_SERIAL);
     }
 
     @Test
@@ -89,6 +100,10 @@
         // Verify timeout and command (split, removed whitespace, and device serial)
         mPreparer.setUp(mTestInfo);
         verify(mRunUtil).runTimedCmd(eq(10L), eq("command"), eq("argument"), eq(DEVICE_SERIAL));
+
+        // No flashing permit taken/returned by default
+        verify(mDeviceManager, never()).takeFlashingPermit();
+        verify(mDeviceManager, never()).returnFlashingPermit();
     }
 
     @Test
@@ -120,6 +135,24 @@
     }
 
     @Test
+    public void testSetUp_flashingPermit() throws Exception {
+        OptionSetter optionSetter = new OptionSetter(mPreparer);
+        optionSetter.setOptionValue("host-setup-command", FULL_COMMAND);
+        optionSetter.setOptionValue("use-flashing-permit", "true");
+
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        when(mRunUtil.runTimedCmd(anyLong(), any())).thenReturn(result);
+
+        // Verify command ran with flashing permit
+        mPreparer.setUp(mTestInfo);
+        InOrder inOrder = inOrder(mRunUtil, mDeviceManager);
+        inOrder.verify(mDeviceManager).takeFlashingPermit();
+        inOrder.verify(mRunUtil)
+                .runTimedCmd(anyLong(), eq("command"), eq("argument"), eq(DEVICE_SERIAL));
+        inOrder.verify(mDeviceManager).returnFlashingPermit();
+    }
+
+    @Test
     public void testTearDown() throws Exception {
         OptionSetter optionSetter = new OptionSetter(mPreparer);
         optionSetter.setOptionValue("host-teardown-command", FULL_COMMAND);
@@ -131,6 +164,10 @@
         // Verify timeout and command (split, removed whitespace, and device serial)
         mPreparer.tearDown(mTestInfo, null);
         verify(mRunUtil).runTimedCmd(eq(10L), eq("command"), eq("argument"), eq(DEVICE_SERIAL));
+
+        // No flashing permit taken/returned by default
+        verify(mDeviceManager, never()).takeFlashingPermit();
+        verify(mDeviceManager, never()).returnFlashingPermit();
     }
 
     @Test
@@ -146,6 +183,24 @@
     }
 
     @Test
+    public void testTearDown_flashingPermit() throws Exception {
+        OptionSetter optionSetter = new OptionSetter(mPreparer);
+        optionSetter.setOptionValue("host-teardown-command", FULL_COMMAND);
+        optionSetter.setOptionValue("use-flashing-permit", "true");
+
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        when(mRunUtil.runTimedCmd(anyLong(), any())).thenReturn(result);
+
+        // Verify command ran with flashing permit
+        mPreparer.tearDown(mTestInfo, null);
+        InOrder inOrder = inOrder(mRunUtil, mDeviceManager);
+        inOrder.verify(mDeviceManager).takeFlashingPermit();
+        inOrder.verify(mRunUtil)
+                .runTimedCmd(anyLong(), eq("command"), eq("argument"), eq(DEVICE_SERIAL));
+        inOrder.verify(mDeviceManager).returnFlashingPermit();
+    }
+
+    @Test
     public void testBgCommand() throws Exception {
         OptionSetter optionSetter = new OptionSetter(mPreparer);
         optionSetter.setOptionValue("host-background-command", FULL_COMMAND);
diff --git a/tests/src/com/android/tradefed/targetprep/RunHostScriptTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/RunHostScriptTargetPreparerTest.java
index 4125fa1..236498a 100644
--- a/tests/src/com/android/tradefed/targetprep/RunHostScriptTargetPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/RunHostScriptTargetPreparerTest.java
@@ -21,6 +21,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -42,6 +43,7 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.Answers;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
@@ -117,6 +119,9 @@
         verify(mRunUtil).runTimedCmd(10L, mScriptFile.getAbsolutePath());
         // Verify that script is executable
         assertTrue(mScriptFile.canExecute());
+        // No flashing permit taken/returned by default
+        verify(mDeviceManager, never()).takeFlashingPermit();
+        verify(mDeviceManager, never()).returnFlashingPermit();
     }
 
     @Test
@@ -155,11 +160,11 @@
     @Test
     public void testSetUp_pathVariable() throws Exception {
         mOptionSetter.setOptionValue("script-file", mScriptFile.getAbsolutePath());
-        // Create and set dummy adb binary
+        // Create and set no-op adb binary
         Path adbDir = Files.createTempDirectory(mWorkDir.toPath(), "adb");
         File adbBinary = File.createTempFile("adb", ".sh", adbDir.toFile());
         when(mTestInfo.executionFiles().get(eq(FilesKey.ADB_BINARY))).thenReturn(adbBinary);
-        // Create and set dummy fastboot binary
+        // Create and set no-op fastboot binary
         Path fastbootDir = Files.createTempDirectory(mWorkDir.toPath(), "fastboot");
         File fastbootBinary = File.createTempFile("fastboot", ".sh", fastbootDir.toFile());
         when(mDeviceManager.getFastbootPath()).thenReturn(fastbootBinary.getAbsolutePath());
@@ -169,4 +174,16 @@
         mPreparer.setUp(mTestInfo);
         verify(mRunUtil).setEnvVariable("PATH", expectedPath);
     }
+
+    @Test
+    public void testSetUp_flashingPermit() throws Exception {
+        mOptionSetter.setOptionValue("script-file", mScriptFile.getAbsolutePath());
+        mOptionSetter.setOptionValue("use-flashing-permit", "true");
+        // Verify script executed with flashing permit
+        mPreparer.setUp(mTestInfo);
+        InOrder inOrder = inOrder(mRunUtil, mDeviceManager);
+        inOrder.verify(mDeviceManager).takeFlashingPermit();
+        inOrder.verify(mRunUtil).runTimedCmd(anyLong(), eq(mScriptFile.getAbsolutePath()));
+        inOrder.verify(mDeviceManager).returnFlashingPermit();
+    }
 }
diff --git a/tests/src/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparerTest.java
new file mode 100644
index 0000000..14e324b
--- /dev/null
+++ b/tests/src/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparerTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.targetprep;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.UserInfo;
+import com.android.tradefed.invoker.TestInformation;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public class RunOnSecondaryUserTargetPreparerTest {
+
+    private static final String CREATED_USER_2_MESSAGE = "Created user id 2";
+
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private TestInformation mTestInfo;
+
+    private RunOnSecondaryUserTargetPreparer mPreparer;
+    private OptionSetter mOptionSetter;
+
+    @Before
+    public void setUp() throws Exception {
+        mPreparer = new RunOnSecondaryUserTargetPreparer();
+        mOptionSetter = new OptionSetter(mPreparer);
+    }
+
+    @Test
+    public void setUp_createsAndStartsSecondaryUser() throws Exception {
+        String expectedCreateUserCommand = "pm create-user secondary";
+        String expectedStartUserCommand = "am start-user -w 2";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_2_MESSAGE);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice()).executeShellCommand(expectedCreateUserCommand);
+        verify(mTestInfo.getDevice()).executeShellCommand(expectedStartUserCommand);
+    }
+
+    @Test
+    public void setUp_secondaryUserAlreadyExists_doesNotCreateSecondaryUser() throws Exception {
+        Map<Integer, UserInfo> userInfos = new HashMap<>();
+        userInfos.put(2, new UserInfo(2, "secondary", /* flag= */ 0, /* isRunning= */ true));
+        when(mTestInfo.getDevice().getUserInfos()).thenReturn(userInfos);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice(), never()).executeShellCommand(any());
+    }
+
+    @Test
+    public void setUp_secondaryUserAlreadyExists_runsTestAsExistingUser() throws Exception {
+        Map<Integer, UserInfo> userInfos = new HashMap<>();
+        userInfos.put(3, new UserInfo(3, "secondary", /* flag= */ 0, /* isRunning= */ true));
+        when(mTestInfo.getDevice().getUserInfos()).thenReturn(userInfos);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.properties())
+                .put(RunOnWorkProfileTargetPreparer.RUN_TESTS_AS_USER_KEY, "3");
+    }
+
+    @Test
+    public void setUp_setsRunTestsAsUser() throws Exception {
+        String expectedCreateUserCommand = "pm create-user secondary";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_2_MESSAGE);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.properties())
+                .put(RunOnSecondaryUserTargetPreparer.RUN_TESTS_AS_USER_KEY, "2");
+    }
+
+    @Test
+    public void setUp_secondaryUserAlreadyExists_installsPackagesInExistingUser() throws Exception {
+        Map<Integer, UserInfo> userInfos = new HashMap<>();
+        userInfos.put(3, new UserInfo(3, "secondary", /* flag= */ 0, /* isRunning= */ true));
+        when(mTestInfo.getDevice().getUserInfos()).thenReturn(userInfos);
+        mOptionSetter.setOptionValue(
+                RunOnWorkProfileTargetPreparer.TEST_PACKAGE_NAME_OPTION, "com.android.testpackage");
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice())
+                .executeShellCommand("pm install-existing --user 3 com.android.testpackage");
+    }
+
+    @Test
+    public void setUp_installsPackagesInSecondaryUser() throws Exception {
+        String expectedCreateUserCommand = "pm create-user secondary";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_2_MESSAGE);
+        mOptionSetter.setOptionValue(
+                RunOnSecondaryUserTargetPreparer.TEST_PACKAGE_NAME_OPTION,
+                "com.android.testpackage");
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice())
+                .executeShellCommand("pm install-existing --user 2 com.android.testpackage");
+    }
+
+    @Test
+    public void setUp_workProfileAlreadyExists_disablesTearDown() throws Exception {
+        Map<Integer, UserInfo> userInfos = new HashMap<>();
+        userInfos.put(3, new UserInfo(3, "secondary", /* flag= */ 0, /* isRunning= */ true));
+        when(mTestInfo.getDevice().getUserInfos()).thenReturn(userInfos);
+        mOptionSetter.setOptionValue("disable-tear-down", "false");
+
+        mPreparer.setUp(mTestInfo);
+
+        assertThat(mPreparer.isTearDownDisabled()).isTrue();
+    }
+
+    @Test
+    public void setUp_doesNotDisableTearDown() throws Exception {
+        String expectedCreateUserCommand = "pm create-user secondary";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_2_MESSAGE);
+        mOptionSetter.setOptionValue("disable-tear-down", "false");
+
+        mPreparer.setUp(mTestInfo);
+
+        assertThat(mPreparer.isTearDownDisabled()).isFalse();
+    }
+
+    @Test
+    public void tearDown_removesSecondaryUser() throws Exception {
+        when(mTestInfo.properties().get(RunOnSecondaryUserTargetPreparer.RUN_TESTS_AS_USER_KEY))
+                .thenReturn("2");
+
+        mPreparer.tearDown(mTestInfo, /* throwable= */ null);
+
+        verify(mTestInfo.getDevice()).removeUser(2);
+    }
+}
diff --git a/tests/src/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparerTest.java
new file mode 100644
index 0000000..5845cd2
--- /dev/null
+++ b/tests/src/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparerTest.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.targetprep;
+
+import static com.android.tradefed.targetprep.RunOnWorkProfileTargetPreparer.RUN_TESTS_AS_USER_KEY;
+import static com.android.tradefed.targetprep.RunOnWorkProfileTargetPreparer.TEST_PACKAGE_NAME_OPTION;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.UserInfo;
+import com.android.tradefed.invoker.TestInformation;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public class RunOnWorkProfileTargetPreparerTest {
+    private static final String CREATED_USER_10_MESSAGE = "Created user id 10";
+
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private TestInformation mTestInfo;
+
+    private RunOnWorkProfileTargetPreparer mPreparer;
+    private OptionSetter mOptionSetter;
+
+    @Before
+    public void setUp() throws Exception {
+        mPreparer = new RunOnWorkProfileTargetPreparer();
+        mOptionSetter = new OptionSetter(mPreparer);
+    }
+
+    @Test
+    public void setUp_createsAndStartsWorkProfile() throws Exception {
+        String expectedCreateUserCommand = "pm create-user --profileOf 0 --managed work";
+        String expectedStartUserCommand = "am start-user -w 10";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_10_MESSAGE);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice()).executeShellCommand(expectedCreateUserCommand);
+        verify(mTestInfo.getDevice()).executeShellCommand(expectedStartUserCommand);
+    }
+
+    @Test
+    public void setUp_workProfileAlreadyExists_doesNotCreateWorkProfile() throws Exception {
+        Map<Integer, UserInfo> userInfos = new HashMap<>();
+        userInfos.put(
+                10,
+                new UserInfo(
+                        10,
+                        "work",
+                        /* flag= */ UserInfo.FLAG_MANAGED_PROFILE,
+                        /* isRunning= */ true));
+        when(mTestInfo.getDevice().getUserInfos()).thenReturn(userInfos);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice(), never()).executeShellCommand(any());
+    }
+
+    @Test
+    public void setUp_nonZeroCurrentUser_createsWorkProfileForCorrectUser() throws Exception {
+        when(mTestInfo.getDevice().getCurrentUser()).thenReturn(1);
+        String expectedCreateUserCommand = "pm create-user --profileOf 1 --managed work";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_10_MESSAGE);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice()).executeShellCommand(expectedCreateUserCommand);
+    }
+
+    @Test
+    public void setUp_workProfileAlreadyExists_runsTestAsExistingUser() throws Exception {
+        Map<Integer, UserInfo> userInfos = new HashMap<>();
+        userInfos.put(
+                11,
+                new UserInfo(
+                        11,
+                        "work",
+                        /* flag= */ UserInfo.FLAG_MANAGED_PROFILE,
+                        /* isRunning= */ true));
+        when(mTestInfo.getDevice().getUserInfos()).thenReturn(userInfos);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.properties()).put(RUN_TESTS_AS_USER_KEY, "11");
+    }
+
+    @Test
+    public void setUp_setsRunTestsAsUser() throws Exception {
+        String expectedCreateUserCommand = "pm create-user --profileOf 0 --managed work";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_10_MESSAGE);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.properties()).put(RUN_TESTS_AS_USER_KEY, "10");
+    }
+
+    @Test
+    public void setUp_workProfileAlreadyExists_installsPackagesInExistingUser() throws Exception {
+        Map<Integer, UserInfo> userInfos = new HashMap<>();
+        userInfos.put(
+                11,
+                new UserInfo(
+                        11,
+                        "work",
+                        /* flag= */ UserInfo.FLAG_MANAGED_PROFILE,
+                        /* isRunning= */ true));
+        when(mTestInfo.getDevice().getUserInfos()).thenReturn(userInfos);
+        mOptionSetter.setOptionValue(TEST_PACKAGE_NAME_OPTION, "com.android.testpackage");
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice())
+                .executeShellCommand("pm install-existing --user 11 com.android.testpackage");
+    }
+
+    @Test
+    public void setUp_installsPackagesInWorkUser() throws Exception {
+        String expectedCreateUserCommand = "pm create-user --profileOf 0 --managed work";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_10_MESSAGE);
+        mOptionSetter.setOptionValue(TEST_PACKAGE_NAME_OPTION, "com.android.testpackage");
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice())
+                .executeShellCommand("pm install-existing --user 10 com.android.testpackage");
+    }
+
+    @Test
+    public void setUp_workProfileAlreadyExists_disablesTearDown() throws Exception {
+        Map<Integer, UserInfo> userInfos = new HashMap<>();
+        userInfos.put(
+                11,
+                new UserInfo(
+                        11,
+                        "work",
+                        /* flag= */ UserInfo.FLAG_MANAGED_PROFILE,
+                        /* isRunning= */ true));
+        when(mTestInfo.getDevice().getUserInfos()).thenReturn(userInfos);
+        mOptionSetter.setOptionValue("disable-tear-down", "false");
+
+        mPreparer.setUp(mTestInfo);
+
+        assertThat(mPreparer.isTearDownDisabled()).isTrue();
+    }
+
+    @Test
+    public void setUp_doesNotDisableTearDown() throws Exception {
+        String expectedCreateUserCommand = "pm create-user --profileOf 0 --managed work";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_10_MESSAGE);
+        mOptionSetter.setOptionValue("disable-tear-down", "false");
+
+        mPreparer.setUp(mTestInfo);
+
+        assertThat(mPreparer.isTearDownDisabled()).isFalse();
+    }
+
+    @Test
+    public void tearDown_removesWorkUser() throws Exception {
+        when(mTestInfo.properties().get(RUN_TESTS_AS_USER_KEY)).thenReturn("10");
+
+        mPreparer.tearDown(mTestInfo, /* throwable= */ null);
+
+        verify(mTestInfo.getDevice()).removeUser(10);
+    }
+}
diff --git a/tests/src/com/android/tradefed/targetprep/multi/MixImageZipPreparerTest.java b/tests/src/com/android/tradefed/targetprep/multi/MixImageZipPreparerTest.java
index e34630a..b0dc5ed 100644
--- a/tests/src/com/android/tradefed/targetprep/multi/MixImageZipPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/multi/MixImageZipPreparerTest.java
@@ -79,7 +79,7 @@
     // The strings written to temporary image files.
     private static final String DEVICE_CONTENT = "device content";
     private static final String SYSTEM_CONTENT = "system content";
-    private static final String DUMMY_CONTENT = "\0";
+    private static final String STUB_CONTENT = "\0";
     private static final String RESOURCE_CONTENT = "resource content";
 
     private IInvocationContext mMockContext;
@@ -165,8 +165,8 @@
         for (String imageName : imageNames) {
             mPreparer.addSystemFileName(imageName);
         }
-        mPreparer.addDummyFileName(PRODUCT_IMAGE_NAME);
-        mPreparer.addDummyFileName("missing_dummy.img");
+        mPreparer.addStubFileName(PRODUCT_IMAGE_NAME);
+        mPreparer.addStubFileName("missing_stub.img");
     }
 
     private void setUpPreparerAndSystem() throws IOException {
@@ -337,7 +337,7 @@
             } else {
                 // setUpDevice was called.
                 verifyImage(SYSTEM_CONTENT, mixedImageDir, SYSTEM_IMAGE_NAME);
-                verifyImage(DUMMY_CONTENT, mixedImageDir, PRODUCT_IMAGE_NAME);
+                verifyImage(STUB_CONTENT, mixedImageDir, PRODUCT_IMAGE_NAME);
             }
 
             if (mResourceBuild != null) {
diff --git a/tests/src/com/android/tradefed/testtype/ArtGTestTest.java b/tests/src/com/android/tradefed/testtype/ArtGTestTest.java
new file mode 100644
index 0000000..a30343e
--- /dev/null
+++ b/tests/src/com/android/tradefed/testtype/ArtGTestTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.testtype;
+
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.MockFileUtil;
+import com.android.tradefed.targetprep.ArtChrootPreparer;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.result.ITestInvocationListener;
+
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ArtGTest}. */
+@RunWith(JUnit4.class)
+public class ArtGTestTest {
+    private ITestInvocationListener mMockInvocationListener = null;
+    private IShellOutputReceiver mMockReceiver = null;
+    private ITestDevice mMockITestDevice = null;
+    private GTest mGTest;
+    private TestInformation mTestInfo;
+
+    @Before
+    public void setUp() throws Exception {
+        mMockInvocationListener = EasyMock.createMock(ITestInvocationListener.class);
+        mMockReceiver = EasyMock.createMock(IShellOutputReceiver.class);
+        mMockITestDevice = EasyMock.createMock(ITestDevice.class);
+        mGTest =
+                new ArtGTest() {
+                    @Override
+                    IShellOutputReceiver createResultParser(
+                            String runName, ITestInvocationListener listener) {
+                        return mMockReceiver;
+                    }
+                };
+        mGTest.setDevice(mMockITestDevice);
+        mGTest.setNativeTestDevicePath(ArtChrootPreparer.CHROOT_PATH);
+        mTestInfo = TestInformation.newBuilder().build();
+
+        EasyMock.expect(mMockITestDevice.getSerialNumber()).andStubReturn("serial");
+    }
+
+    private void replayMocks() {
+        EasyMock.replay(mMockInvocationListener, mMockITestDevice);
+    }
+
+    private void verifyMocks() {
+        EasyMock.verify(mMockInvocationListener, mMockITestDevice);
+    }
+
+    @Test
+    public void testChroot_testRun() throws DeviceNotAvailableException {
+        final String nativeTestPath = ArtChrootPreparer.CHROOT_PATH;
+        final String test1 = "test1";
+        final String testPath1 = String.format("%s/%s", nativeTestPath, test1);
+
+        MockFileUtil.setMockDirContents(mMockITestDevice, nativeTestPath, test1);
+        EasyMock.expect(mMockITestDevice.doesFileExist(nativeTestPath)).andReturn(true);
+        EasyMock.expect(mMockITestDevice.isDirectory(nativeTestPath)).andReturn(true);
+        EasyMock.expect(mMockITestDevice.isDirectory(testPath1)).andReturn(false);
+        EasyMock.expect(mMockITestDevice.isExecutable(testPath1)).andReturn(true);
+
+        String[] files = new String[] {"test1"};
+        EasyMock.expect(mMockITestDevice.getChildren(nativeTestPath)).andReturn(files);
+        mMockITestDevice.executeShellCommand(
+                EasyMock.startsWith("chroot /data/local/tmp/art-test-chroot /test1"),
+                EasyMock.anyObject(),
+                EasyMock.anyLong(),
+                EasyMock.anyObject(),
+                EasyMock.anyInt());
+
+        replayMocks();
+        mGTest.run(mTestInfo, mMockInvocationListener);
+        verifyMocks();
+    }
+}
diff --git a/tests/src/com/android/tradefed/testtype/ArtRunTestTest.java b/tests/src/com/android/tradefed/testtype/ArtRunTestTest.java
index 23d8b75..9b360e4 100644
--- a/tests/src/com/android/tradefed/testtype/ArtRunTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/ArtRunTestTest.java
@@ -25,7 +25,6 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.TestInformation;
-import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.TestDescription;
@@ -49,9 +48,6 @@
 @RunWith(JUnit4.class)
 public class ArtRunTestTest {
 
-    // Default run-test name.
-    private static final String RUN_TEST_NAME = "run-test";
-
     private ITestInvocationListener mMockInvocationListener;
     private IAbi mMockAbi;
     private ITestDevice mMockITestDevice;
@@ -60,9 +56,9 @@
     private ArtRunTest mArtRunTest;
     private OptionSetter mSetter;
     private TestInformation mTestInfo;
-    // Target tests directory.
-    private File mTmpTargetTestsDir;
-    // Expected output file (under the target tests directory).
+    // Test dependencies directory on host.
+    private File mTmpDepsDir;
+    // Expected output file (within the dependencies directory).
     private File mTmpExpectedFile;
 
     @Before
@@ -82,23 +78,22 @@
         mArtRunTest.setDevice(mMockITestDevice);
         mSetter = new OptionSetter(mArtRunTest);
 
-        // Set up target tests directory and expected output file.
-        mTmpTargetTestsDir = FileUtil.createTempDir("target_testcases");
-        File runTestDir = new File(mTmpTargetTestsDir, RUN_TEST_NAME);
-        runTestDir.mkdir();
-        mTmpExpectedFile = new File(runTestDir, "expected.txt");
-        FileWriter fw = new FileWriter(mTmpExpectedFile);
-        fw.write("output\n");
-        fw.close();
-
-        // Set the target tests directory in test information object.
-        mTestInfo = TestInformation.newBuilder().build();
-        mTestInfo.executionFiles().put(FilesKey.TARGET_TESTS_DIRECTORY, mTmpTargetTestsDir);
+        // Temporary test directory (e.g. for the expected-output file).
+        mTmpDepsDir = FileUtil.createTempDir("art-run-test-deps");
+        mTestInfo = TestInformation.newBuilder().setDependenciesFolder(mTmpDepsDir).build();
     }
 
     @After
     public void tearDown() {
-        FileUtil.recursiveDelete(mTmpTargetTestsDir);
+        FileUtil.recursiveDelete(mTmpDepsDir);
+    }
+
+    /** Helper creating an expected-output file within the (temporary) test directory. */
+    private void createExpectedOutputFile(String runTestName) throws IOException {
+        mTmpExpectedFile = new File(mTmpDepsDir, runTestName + "-expected.txt");
+        FileWriter fw = new FileWriter(mTmpExpectedFile);
+        fw.write("output\n");
+        fw.close();
     }
 
     /** Helper mocking writing the output of a test command. */
@@ -151,7 +146,9 @@
     @Test
     public void testRunSingleTest_unsetClasspathOption()
             throws ConfigurationException, DeviceNotAvailableException, IOException {
-        mSetter.setOptionValue("run-test-name", RUN_TEST_NAME);
+        final String runTestName = "test";
+        mSetter.setOptionValue("run-test-name", runTestName);
+        createExpectedOutputFile(runTestName);
 
         replayMocks();
         try {
@@ -163,21 +160,20 @@
         verifyMocks();
     }
 
-    /** Test the run method for a (single) test. */
-    @Test
-    public void testRunSingleTest()
+    /** Helper containing testing logic for a (single) test expected to run (and succeed). */
+    private void doTestRunSingleTest(final String runTestName, final String classpath)
             throws ConfigurationException, DeviceNotAvailableException, IOException {
-        mSetter.setOptionValue("run-test-name", RUN_TEST_NAME);
-        final String classpath = "/data/local/tmp/test/test.jar";
+        mSetter.setOptionValue("run-test-name", runTestName);
+        createExpectedOutputFile(runTestName);
         mSetter.setOptionValue("classpath", classpath);
 
         // Pre-test checks.
-        EasyMock.expect(mMockITestDevice.getSerialNumber()).andReturn("");
         EasyMock.expect(mMockAbi.getName()).andReturn("abi");
+        EasyMock.expect(mMockITestDevice.getSerialNumber()).andReturn("");
         String runName = "ArtRunTest_abi";
         // Beginning of test.
         mMockInvocationListener.testRunStarted(runName, 1);
-        TestDescription testId = new TestDescription(runName, RUN_TEST_NAME);
+        TestDescription testId = new TestDescription(runName, runTestName);
         mMockInvocationListener.testStarted(testId);
         String cmd = String.format("dalvikvm64 -classpath %s Main", classpath);
         // Test execution.
@@ -198,6 +194,31 @@
         verifyMocks();
     }
 
+    /** Helper containing testing logic for a (single) test expected not to run. */
+    private void doTestDoNotRunSingleTest(final String runTestName, final String classpath)
+            throws ConfigurationException, DeviceNotAvailableException, IOException {
+        mSetter.setOptionValue("run-test-name", runTestName);
+        createExpectedOutputFile(runTestName);
+        mSetter.setOptionValue("classpath", classpath);
+
+        EasyMock.expect(mMockAbi.getName()).andReturn("abi");
+        replayMocks();
+
+        mArtRunTest.run(mTestInfo, mMockInvocationListener);
+
+        verifyMocks();
+    }
+
+    /** Test the run method for a (single) test. */
+    @Test
+    public void testRunSingleTest()
+            throws ConfigurationException, DeviceNotAvailableException, IOException {
+        final String runTestName = "test";
+        final String classpath = "/data/local/tmp/test/test.jar";
+
+        doTestRunSingleTest(runTestName, classpath);
+    }
+
     /**
      * Test the behavior of the run method when the output produced by the shell command on device
      * differs from the expected output.
@@ -205,17 +226,19 @@
     @Test
     public void testRunSingleTest_unexpectedOutput()
             throws ConfigurationException, DeviceNotAvailableException, IOException {
-        mSetter.setOptionValue("run-test-name", RUN_TEST_NAME);
+        final String runTestName = "test";
+        mSetter.setOptionValue("run-test-name", runTestName);
+        createExpectedOutputFile(runTestName);
         final String classpath = "/data/local/tmp/test/test.jar";
         mSetter.setOptionValue("classpath", classpath);
 
         // Pre-test checks.
-        EasyMock.expect(mMockITestDevice.getSerialNumber()).andReturn("");
         EasyMock.expect(mMockAbi.getName()).andReturn("abi");
+        EasyMock.expect(mMockITestDevice.getSerialNumber()).andReturn("");
         String runName = "ArtRunTest_abi";
         // Beginning of test.
         mMockInvocationListener.testRunStarted(runName, 1);
-        TestDescription testId = new TestDescription(runName, RUN_TEST_NAME);
+        TestDescription testId = new TestDescription(runName, runTestName);
         mMockInvocationListener.testStarted(testId);
         String cmd = String.format("dalvikvm64 -classpath %s Main", classpath);
         // Test execution.
@@ -236,4 +259,44 @@
 
         verifyMocks();
     }
+
+    /** Test the run method for a (single) test contained in an include filter. */
+    @Test
+    public void testIncludeFilter()
+            throws ConfigurationException, DeviceNotAvailableException, IOException {
+        final String runTestName = "test";
+        final String classpath = "/data/local/tmp/test/test.jar";
+        // Add an include filter containing the test's name.
+        mArtRunTest.addIncludeFilter(runTestName);
+
+        doTestRunSingleTest(runTestName, classpath);
+    }
+
+    /** Test the run method for a (single) test contained in an exclude filter. */
+    @Test
+    public void testExcludeFilter()
+            throws ConfigurationException, DeviceNotAvailableException, IOException {
+        final String runTestName = "test";
+        final String classpath = "/data/local/tmp/test/test.jar";
+        // Add an exclude filter containing the test's name.
+        mArtRunTest.addExcludeFilter(runTestName);
+
+        doTestDoNotRunSingleTest(runTestName, classpath);
+    }
+
+    /**
+     * Test the run method for a (single) test contained both in an include and an exclude filter.
+     */
+    @Test
+    public void testIncludeAndExcludeFilter()
+            throws ConfigurationException, DeviceNotAvailableException, IOException {
+        final String runTestName = "test";
+        final String classpath = "/data/local/tmp/test/test.jar";
+        // Add an include filter containing the test's name.
+        mArtRunTest.addIncludeFilter(runTestName);
+        // Add an exclude filter containing the test's name.
+        mArtRunTest.addExcludeFilter(runTestName);
+
+        doTestDoNotRunSingleTest(runTestName, classpath);
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/GTestTest.java b/tests/src/com/android/tradefed/testtype/GTestTest.java
index b4dbe2d..7f05104 100644
--- a/tests/src/com/android/tradefed/testtype/GTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestTest.java
@@ -28,10 +28,13 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.MockFileUtil;
+import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.testtype.coverage.CoverageOptions;
 
+import com.google.common.collect.ImmutableList;
+
 import org.easymock.EasyMock;
 import org.junit.Before;
 import org.junit.Test;
@@ -39,8 +42,6 @@
 import org.junit.runners.JUnit4;
 
 import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.concurrent.TimeUnit;
 
 
@@ -48,6 +49,7 @@
 @RunWith(JUnit4.class)
 public class GTestTest {
     private static final String GTEST_FLAG_FILTER = "--gtest_filter";
+    private IInvocationContext mMockContext = null;
     private ITestInvocationListener mMockInvocationListener = null;
     private IShellOutputReceiver mMockReceiver = null;
     private ITestDevice mMockITestDevice = null;
@@ -62,12 +64,15 @@
     /** Helper to initialize the various EasyMocks we'll need. */
     @Before
     public void setUp() throws Exception {
+        mMockContext = EasyMock.createMock(IInvocationContext.class);
         mMockInvocationListener = EasyMock.createMock(ITestInvocationListener.class);
         mMockReceiver = EasyMock.createMock(IShellOutputReceiver.class);
         mMockITestDevice = EasyMock.createMock(ITestDevice.class);
         mMockReceiver.flush();
         EasyMock.expectLastCall().anyTimes();
         EasyMock.expect(mMockITestDevice.getSerialNumber()).andStubReturn("serial");
+        EasyMock.expect(mMockContext.getDevices())
+                .andStubReturn(ImmutableList.of(mMockITestDevice));
         mGTest =
                 new GTest() {
                     @Override
@@ -98,21 +103,21 @@
         mConfiguration.setCoverageOptions(mCoverageOptions);
         mGTest.setConfiguration(mConfiguration);
 
-        mTestInfo = TestInformation.newBuilder().build();
+        mTestInfo = TestInformation.newBuilder().setInvocationContext(mMockContext).build();
     }
 
     /**
      * Helper that replays all mocks.
      */
     private void replayMocks() {
-      EasyMock.replay(mMockInvocationListener, mMockITestDevice, mMockReceiver);
+        EasyMock.replay(mMockContext, mMockInvocationListener, mMockITestDevice, mMockReceiver);
     }
 
     /**
      * Helper that verifies all mocks.
      */
     private void verifyMocks() {
-      EasyMock.verify(mMockInvocationListener, mMockITestDevice, mMockReceiver);
+        EasyMock.verify(mMockContext, mMockInvocationListener, mMockITestDevice, mMockReceiver);
     }
 
     /** Test run when the test dir is not found on the device. */
@@ -489,124 +494,6 @@
         verifyMocks();
     }
 
-    /** Test cross-process coverage dump for all native processes */
-    @Test
-    public void testNativeCoverageAllProcesses() throws Exception {
-        mCoverageOptionsSetter.setOptionValue("coverage", "true");
-        mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "GCOV");
-        mCoverageOptionsSetter.setOptionValue("coverage-flush", "true");
-
-        final String nativeTestPath = GTest.DEFAULT_NATIVETEST_PATH;
-        final String test1 = "test1";
-        final String test2 = "test2";
-        final String testPath1 = String.format("%s/%s", nativeTestPath, test1);
-        final String testPath2 = String.format("%s/%s", nativeTestPath, test2);
-
-        MockFileUtil.setMockDirContents(mMockITestDevice, nativeTestPath, test1, test2);
-        EasyMock.expect(mMockITestDevice.enableAdbRoot()).andReturn(true);
-        EasyMock.expect(mMockITestDevice.executeShellCommand("mkdir /data/misc/trace/testcoverage"))
-                .andReturn("");
-        EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
-        EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 -1")).andReturn("");
-        // Wait up to 5 minutes for the device to be available after flushing coverage data.
-        mMockITestDevice.waitForDeviceAvailable(5 * 60 * 1000);
-        EasyMock.expect(mMockITestDevice.executeShellCommand("rm -rf /data/misc/trace/*"))
-                .andReturn("");
-        EasyMock.expect(mMockITestDevice.doesFileExist(nativeTestPath)).andReturn(true);
-        EasyMock.expect(mMockITestDevice.isDirectory(nativeTestPath)).andReturn(true);
-        EasyMock.expect(mMockITestDevice.isDirectory(testPath1)).andReturn(false);
-        // report the file as executable
-        EasyMock.expect(mMockITestDevice.isExecutable(testPath1)).andReturn(true);
-        EasyMock.expect(mMockITestDevice.isDirectory(testPath2)).andReturn(false);
-        // report the file as executable
-        EasyMock.expect(mMockITestDevice.isExecutable(testPath2)).andReturn(true);
-
-        String[] files = new String[] {"test1", "test2"};
-        EasyMock.expect(mMockITestDevice.getChildren(nativeTestPath)).andReturn(files);
-        mMockITestDevice.executeShellCommand(
-                EasyMock.contains(test1),
-                EasyMock.same(mMockReceiver),
-                EasyMock.anyLong(),
-                (TimeUnit) EasyMock.anyObject(),
-                EasyMock.anyInt());
-        mMockITestDevice.executeShellCommand(
-                EasyMock.contains(test2),
-                EasyMock.same(mMockReceiver),
-                EasyMock.anyLong(),
-                (TimeUnit) EasyMock.anyObject(),
-                EasyMock.anyInt());
-
-        replayMocks();
-
-        mGTest.run(mTestInfo, mMockInvocationListener);
-        verifyMocks();
-    }
-
-    /** Test cross-process coverage dump for specific processes */
-    @Test
-    public void testNativeCoverageSpecificProcesses() throws Exception {
-        final List<String> processNames = new ArrayList<>();
-        processNames.add("init");
-        processNames.add("surfaceflinger");
-
-        mCoverageOptionsSetter.setOptionValue("coverage", "true");
-        mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "GCOV");
-        mCoverageOptionsSetter.setOptionValue("coverage-flush", "true");
-        for (String processName : processNames) {
-            mCoverageOptionsSetter.setOptionValue("coverage-processes", processName);
-        }
-
-        final String nativeTestPath = GTest.DEFAULT_NATIVETEST_PATH;
-        final String test1 = "test1";
-        final String test2 = "test2";
-        final String testPath1 = String.format("%s/%s", nativeTestPath, test1);
-        final String testPath2 = String.format("%s/%s", nativeTestPath, test2);
-
-        MockFileUtil.setMockDirContents(mMockITestDevice, nativeTestPath, test1, test2);
-        EasyMock.expect(mMockITestDevice.enableAdbRoot()).andReturn(true);
-        EasyMock.expect(mMockITestDevice.executeShellCommand("mkdir /data/misc/trace/testcoverage"))
-                .andReturn("");
-        // Get the pids to flush coverage data.
-        EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
-        EasyMock.expect(mMockITestDevice.getProcessPid(processNames.get(0))).andReturn("1");
-        EasyMock.expect(mMockITestDevice.getProcessPid(processNames.get(1))).andReturn("1000");
-        EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 1 1000")).andReturn("");
-        // Wait up to 5 minutes for the device to be available after flushing coverage data.
-        mMockITestDevice.waitForDeviceAvailable(5 * 60 * 1000);
-
-        // Clear the coverage data.
-        EasyMock.expect(mMockITestDevice.executeShellCommand("rm -rf /data/misc/trace/*"))
-                .andReturn("");
-        EasyMock.expect(mMockITestDevice.doesFileExist(nativeTestPath)).andReturn(true);
-        EasyMock.expect(mMockITestDevice.isDirectory(nativeTestPath)).andReturn(true);
-        EasyMock.expect(mMockITestDevice.isDirectory(testPath1)).andReturn(false);
-        // report the file as executable
-        EasyMock.expect(mMockITestDevice.isExecutable(testPath1)).andReturn(true);
-        EasyMock.expect(mMockITestDevice.isDirectory(testPath2)).andReturn(false);
-        // report the file as executable
-        EasyMock.expect(mMockITestDevice.isExecutable(testPath2)).andReturn(true);
-
-        String[] files = new String[] {"test1", "test2"};
-        EasyMock.expect(mMockITestDevice.getChildren(nativeTestPath)).andReturn(files);
-        mMockITestDevice.executeShellCommand(
-                EasyMock.contains(test1),
-                EasyMock.same(mMockReceiver),
-                EasyMock.anyLong(),
-                (TimeUnit) EasyMock.anyObject(),
-                EasyMock.anyInt());
-        mMockITestDevice.executeShellCommand(
-                EasyMock.contains(test2),
-                EasyMock.same(mMockReceiver),
-                EasyMock.anyLong(),
-                (TimeUnit) EasyMock.anyObject(),
-                EasyMock.anyInt());
-
-        replayMocks();
-
-        mGTest.run(mTestInfo, mMockInvocationListener);
-        verifyMocks();
-    }
-
     @Test
     public void testGetFileName() {
         String expected = "bar";
diff --git a/tests/src/com/android/tradefed/testtype/GoogleBenchmarkTestTest.java b/tests/src/com/android/tradefed/testtype/GoogleBenchmarkTestTest.java
index 51877ce..8ce4d0a 100644
--- a/tests/src/com/android/tradefed/testtype/GoogleBenchmarkTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/GoogleBenchmarkTestTest.java
@@ -23,6 +23,7 @@
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.util.StringEscapeUtils;
 
 import junit.framework.TestCase;
 
@@ -348,8 +349,7 @@
         String filterFlag = mGoogleBenchmarkTest.getFilterFlagForFilters(filters);
         assertEquals(
                 String.format(
-                        " %s=%s",
-                        GoogleBenchmarkTest.GBENCHMARK_FILTER_OPTION, "filter1\\|filter2"),
+                        " %s=%s", GoogleBenchmarkTest.GBENCHMARK_FILTER_OPTION, "filter1|filter2"),
                 filterFlag);
     }
 
@@ -366,8 +366,7 @@
         String filterFlag = mGoogleBenchmarkTest.getFilterFlagForTests(tests);
         assertEquals(
                 String.format(
-                        " %s=%s",
-                        GoogleBenchmarkTest.GBENCHMARK_FILTER_OPTION, "^test1$\\|^test2$"),
+                        " %s=%s", GoogleBenchmarkTest.GBENCHMARK_FILTER_OPTION, "^test1$|^test2$"),
                 filterFlag);
     }
 
@@ -407,7 +406,10 @@
             String incFilterFlag =
                     mGoogleBenchmarkTest.getFilterFlagForFilters(
                             mGoogleBenchmarkTest.getIncludeFilters());
-            EasyMock.expect(mMockITestDevice.executeShellCommand(EasyMock.contains(incFilterFlag)))
+            EasyMock.expect(
+                            mMockITestDevice.executeShellCommand(
+                                    EasyMock.contains(
+                                            StringEscapeUtils.escapeShell(incFilterFlag))))
                     .andReturn(incTests);
         } else {
             EasyMock.expect(
@@ -422,14 +424,17 @@
             String excFilterFlag =
                     mGoogleBenchmarkTest.getFilterFlagForFilters(
                             mGoogleBenchmarkTest.getExcludeFilters());
-            EasyMock.expect(mMockITestDevice.executeShellCommand(EasyMock.contains(excFilterFlag)))
+            EasyMock.expect(
+                            mMockITestDevice.executeShellCommand(
+                                    EasyMock.contains(
+                                            StringEscapeUtils.escapeShell(excFilterFlag))))
                     .andReturn(excTests);
         }
         if (filteredTests != null && filteredTests.size() > 0) {
             // Runningt filtered tests
             String testFilterFlag = mGoogleBenchmarkTest.getFilterFlagForTests(filteredTests);
             mMockITestDevice.executeShellCommand(
-                    EasyMock.contains(testFilterFlag),
+                    EasyMock.contains(StringEscapeUtils.escapeShell(testFilterFlag)),
                     EasyMock.same(mMockReceiver),
                     EasyMock.anyLong(),
                     (TimeUnit) EasyMock.anyObject(),
diff --git a/tests/src/com/android/tradefed/testtype/HostTestTest.java b/tests/src/com/android/tradefed/testtype/HostTestTest.java
index 570d0fd..3f03776 100644
--- a/tests/src/com/android/tradefed/testtype/HostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/HostTestTest.java
@@ -37,6 +37,8 @@
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
 import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
@@ -346,7 +348,8 @@
 
         @After
         public void tearDown() throws Exception {
-            throw new DeviceNotAvailableException("dnae", "serial");
+            throw new DeviceNotAvailableException(
+                    "dnae", "serial", DeviceErrorIdentifier.DEVICE_UNAVAILABLE);
         }
     }
 
@@ -1323,7 +1326,7 @@
                 EasyMock.eq(test1),
                 EasyMock.contains("MultipleFailureException, There were 2 errors:"));
         mListener.testEnded(EasyMock.eq(test1), (HashMap<String, Metric>) EasyMock.anyObject());
-        Capture<String> captureRunFailure = new Capture<>();
+        Capture<FailureDescription> captureRunFailure = new Capture<>();
         mListener.testRunFailed(EasyMock.capture(captureRunFailure));
         mListener.testRunEnded(EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
         EasyMock.replay(mListener);
@@ -1334,10 +1337,12 @@
             // Expected
         }
         EasyMock.verify(mListener);
-        String failure = captureRunFailure.getValue();
+        FailureDescription failure = captureRunFailure.getValue();
         assertTrue(
-                failure.startsWith(
-                        "Failed with trace: com.android.tradefed.device.DeviceNotAvailableException: dnae"));
+                failure.getErrorMessage()
+                        .startsWith(
+                                "com.android.tradefed.device.DeviceNotAvailableException: dnae"));
+        assertEquals(FailureStatus.LOST_SYSTEM_UNDER_TEST, failure.getFailureStatus());
     }
 
     /**
@@ -2178,7 +2183,8 @@
         setter.setOptionValue("class", Junit4TestClass.class.getName());
         // First class fail with the run failure
         mListener.testRunStarted(EasyMock.anyObject(), EasyMock.eq(1));
-        mListener.testRunFailed(EasyMock.contains("Failed with trace:"));
+        Capture<FailureDescription> capture = new Capture<>();
+        mListener.testRunFailed(EasyMock.capture(capture));
         mListener.testRunEnded(EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
 
         // Second class run properly
@@ -2195,6 +2201,8 @@
         assertEquals(3, mHostTest.countTestCases());
         mHostTest.run(mTestInfo, mListener);
         EasyMock.verify(mListener);
+        FailureDescription failure = capture.getValue();
+        assertEquals("Exception with no error message", failure.getErrorMessage());
     }
 
     /** JUnit4 class that throws within its @Before */
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
index def0130..01d7a60 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
@@ -16,6 +16,8 @@
 
 package com.android.tradefed.testtype;
 
+import static com.android.tradefed.testtype.InstrumentationTest.RUN_TESTS_AS_USER_KEY;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
@@ -24,10 +26,9 @@
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyCollectionOf;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -54,7 +55,6 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.ITestLifeCycleReceiver;
 import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.testtype.coverage.CoverageOptions;
@@ -106,6 +106,7 @@
     private TestInformation mTestInfo = null;
     private CoverageOptions mCoverageOptions = null;
     private OptionSetter mCoverageOptionsSetter = null;
+    private IInvocationContext mContext = null;
 
     // The mock objects.
     @Mock IDevice mMockIDevice;
@@ -160,8 +161,9 @@
 
         mConfig.setCoverageOptions(mCoverageOptions);
         mInstrumentationTest.setConfiguration(mConfig);
-        IInvocationContext context = new InvocationContext();
-        mTestInfo = TestInformation.newBuilder().setInvocationContext(context).build();
+        mContext = new InvocationContext();
+        mContext.addAllocatedDevice("main", mMockTestDevice);
+        mTestInfo = TestInformation.newBuilder().setInvocationContext(mContext).build();
     }
 
     /** Test normal run scenario. */
@@ -202,6 +204,27 @@
     }
 
     @Test
+    public void testRun_nullTestInfo() throws Exception {
+        mInstrumentationTest.run(/* testInfo= */ null, mMockListener);
+
+        verify(mMockTestDevice, atLeastOnce())
+                .runInstrumentationTests(
+                        any(IRemoteAndroidTestRunner.class), any(ITestInvocationListener.class));
+    }
+
+    @Test
+    public void testRun_runTestsAsUser() throws DeviceNotAvailableException {
+        mTestInfo.properties().put(RUN_TESTS_AS_USER_KEY, "10");
+        mInstrumentationTest.run(mTestInfo, mMockListener);
+
+        verify(mMockTestDevice, atLeastOnce())
+                .runInstrumentationTestsAsUser(
+                        any(IRemoteAndroidTestRunner.class),
+                        eq(10),
+                        any(ITestInvocationListener.class));
+    }
+
+    @Test
     public void testRun_bothAbi() throws DeviceNotAvailableException {
         mInstrumentationTest.setAbi(mock(IAbi.class));
         mInstrumentationTest.setForceAbi("test");
@@ -719,51 +742,6 @@
         inOrder.verifyNoMoreInteractions();
     }
 
-    /** Verify that all tests are re-run when there is a failure during a coverage run. */
-    @Test
-    public void testRun_mergedCoverage()
-            throws ConfigurationException, DeviceNotAvailableException {
-        mInstrumentationTest.setRerunMode(true);
-        mInstrumentationTest.setMergeCoverageMeasurements(true);
-        mCoverageOptionsSetter.setOptionValue("coverage", "true");
-        mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "JACOCO");
-
-        // Mock collected tests
-        RunInstrumentationTestsAnswer runTests =
-                (runner, listener) -> {
-                    // perform call back on listener to show run of two tests
-                    listener.testRunStarted(TEST_PACKAGE_VALUE, 2);
-                    listener.testStarted(TEST1);
-                    listener.testEnded(TEST1, EMPTY_STRING_MAP);
-                    listener.testStarted(TEST2);
-                    listener.testEnded(TEST2, EMPTY_STRING_MAP);
-                    listener.testRunEnded(1, EMPTY_STRING_MAP);
-                    return true;
-                };
-
-        doAnswer(runTests)
-                .when(mMockTestDevice)
-                .runInstrumentationTests(
-                        any(IRemoteAndroidTestRunner.class), any(ITestLifeCycleReceiver.class));
-        doReturn(true).when(mMockTestDevice).enableAdbRoot();
-        doReturn("").when(mMockTestDevice).executeShellCommand(anyString());
-
-        mInstrumentationTest.run(mTestInfo, mMockListener);
-
-        InOrder inOrder = Mockito.inOrder(mMockListener);
-        inOrder.verify(mMockListener).testRunStarted(TEST_PACKAGE_VALUE, 2);
-        inOrder.verify(mMockListener).testStarted(eq(TEST1), anyLong());
-        inOrder.verify(mMockListener).testEnded(eq(TEST1), anyLong(), eq(EMPTY_STRING_MAP));
-        inOrder.verify(mMockListener).testStarted(eq(TEST2), anyLong());
-        inOrder.verify(mMockListener).testEnded(eq(TEST2), anyLong(), eq(EMPTY_STRING_MAP));
-        inOrder.verify(mMockListener).testRunEnded(1, EMPTY_STRING_MAP);
-        inOrder.verify(mMockListener).testRunStarted(eq("mergeCoverageMeasurements"), anyInt());
-        inOrder.verify(mMockListener)
-                .testLog(eq("merged_runtime_coverage"), eq(LogDataType.COVERAGE), any());
-        inOrder.verify(mMockListener).testRunEnded(anyLong(), eq(EMPTY_STRING_MAP));
-        inOrder.verifyNoMoreInteractions();
-    }
-
     /** Test the reboot before re-run option. */
     @Test
     public void testRun_rebootBeforeReRun() throws DeviceNotAvailableException {
@@ -1074,8 +1052,9 @@
         inOrder.verify(mMockListener).testRunStarted("fakeName", 1);
         TestDescription tid = new TestDescription("fakeclass", "fakemethod0");
         inOrder.verify(mMockListener).testStarted(tid, 0L);
-        inOrder.verify(mMockListener)
-                .testFailed(tid, "Instrumentation run failed due to 'Process crashed.'");
+        FailureDescription failure =
+                FailureDescription.create("Instrumentation run failed due to 'Process crashed.'");
+        inOrder.verify(mMockListener).testFailed(tid, failure);
         inOrder.verify(mMockListener).testEnded(tid, 15L, EMPTY_STRING_MAP);
         inOrder.verify(mMockListener)
                 .testRunFailed(
@@ -1085,32 +1064,6 @@
         inOrder.verify(mMockListener).testRunEnded(1, EMPTY_STRING_MAP);
     }
 
-    @Test
-    public void testAddCoverageListener_enabled() throws ConfigurationException {
-        mCoverageOptionsSetter.setOptionValue("coverage", "true");
-        mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "GCOV");
-        mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "JACOCO");
-
-        ITestInvocationListener listener =
-                mInstrumentationTest.addJavaCoverageListenerIfEnabled(mMockListener);
-        assertThat(listener).isInstanceOf(JavaCodeCoverageListener.class);
-
-        listener = mInstrumentationTest.addGcovCoverageListenerIfEnabled(mMockListener);
-        assertThat(listener).isInstanceOf(NativeCodeCoverageListener.class);
-    }
-
-    @Test
-    public void testAddCoverageListener_disabled() throws ConfigurationException {
-        mCoverageOptionsSetter.setOptionValue("coverage", "false");
-
-        ITestInvocationListener listener =
-                mInstrumentationTest.addJavaCoverageListenerIfEnabled(mMockListener);
-        assertThat(listener).isSameAs(mMockListener);
-
-        listener = mInstrumentationTest.addGcovCoverageListenerIfEnabled(mMockListener);
-        assertThat(listener).isSameAs(mMockListener);
-    }
-
     /** Test normal run scenario when {@link IMetricCollector} are specified. */
     @Test
     public void testRun_withCollectors() throws DeviceNotAvailableException {
diff --git a/tests/src/com/android/tradefed/testtype/NoisyDryRunTestTest.java b/tests/src/com/android/tradefed/testtype/NoisyDryRunTestTest.java
index 2c3d663..2b46263 100644
--- a/tests/src/com/android/tradefed/testtype/NoisyDryRunTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/NoisyDryRunTestTest.java
@@ -366,6 +366,30 @@
         verifyMocks();
     }
 
+    @Test
+    public void testRun_withDelegation() throws Exception {
+        FileUtil.writeToFile("tf/fake --delegated-tf .\n" + "tf/fake", mFile);
+        mMockListener.testRunStarted("com.android.tradefed.testtype.NoisyDryRunTest_parseFile", 1);
+        mMockListener.testStarted(anyObject());
+        mMockListener.testEnded(anyObject(), EasyMock.<HashMap<String, Metric>>anyObject());
+        mMockListener.testRunEnded(EasyMock.eq(0l), EasyMock.<HashMap<String, Metric>>anyObject());
+
+        mMockListener.testRunStarted(
+                "com.android.tradefed.testtype.NoisyDryRunTest_parseCommands", 2);
+        mMockListener.testStarted(anyObject());
+        mMockListener.testEnded(anyObject(), EasyMock.<HashMap<String, Metric>>anyObject());
+        mMockListener.testStarted(anyObject());
+        mMockListener.testEnded(anyObject(), EasyMock.<HashMap<String, Metric>>anyObject());
+        mMockListener.testRunEnded(EasyMock.eq(0l), EasyMock.<HashMap<String, Metric>>anyObject());
+        replayMocks();
+
+        NoisyDryRunTest noisyDryRunTest = new NoisyDryRunTest();
+        OptionSetter setter = new OptionSetter(noisyDryRunTest);
+        setter.setOptionValue("cmdfile", mFile.getAbsolutePath());
+        noisyDryRunTest.run(mTestInfo, mMockListener);
+        verifyMocks();
+    }
+
     private void replayMocks() {
         EasyMock.replay(mMockListener, mMockRunUtil);
     }
diff --git a/tests/src/com/android/tradefed/testtype/binary/ExecutableTargetTestTest.java b/tests/src/com/android/tradefed/testtype/binary/ExecutableTargetTestTest.java
index ba18dc0..5c628df 100644
--- a/tests/src/com/android/tradefed/testtype/binary/ExecutableTargetTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/binary/ExecutableTargetTestTest.java
@@ -52,7 +52,6 @@
     private final String testCmd2 = "cmd2";
     private final String testName3 = "testName3";
     private final String testCmd3 = "cmd3";
-    private static final String NO_BINARY_ERROR = "Binary %s does not exist.";
     private static final String ERROR_MESSAGE = "binary returned non-zero exit code.";
 
     private ITestInvocationListener mListener = null;
@@ -376,7 +375,7 @@
 
     /** Test split() for sharding */
     @Test
-    public void testShard_Split() throws DeviceNotAvailableException, ConfigurationException {
+    public void testShard_Split() throws ConfigurationException {
         mExecutableTargetTest = new ExecutableTargetTest();
         // Set test commands
         OptionSetter setter = new OptionSetter(mExecutableTargetTest);
diff --git a/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java b/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
index de69a05..27e793b 100644
--- a/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
@@ -28,6 +28,9 @@
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -78,6 +81,7 @@
     private File mMoblyTestDir;
     private File mMoblyBinary; // used by python-binaries option
     private File mMoblyBinary2; // used by par-file-name option
+    private File mVenvDir;
     private DeviceBuildInfo mMockBuildInfo;
 
     @Before
@@ -87,6 +91,10 @@
         mMockRunUtil = Mockito.mock(IRunUtil.class);
         mMockBuildInfo = Mockito.mock(DeviceBuildInfo.class);
         mSpyTest.setDevice(mMockDevice);
+
+        mVenvDir = FileUtil.createTempDir("venv");
+        new File(mVenvDir, "bin").mkdir();
+
         Mockito.doReturn(mMockRunUtil).when(mSpyTest).getRunUtil();
         Mockito.doReturn(DEFAULT_TIME_OUT).when(mSpyTest).getTestTimeout();
         Mockito.doReturn("not_adb").when(mSpyTest).getAdbPath();
@@ -99,6 +107,7 @@
     @After
     public void tearDown() throws Exception {
         FileUtil.recursiveDelete(mMoblyTestDir);
+        FileUtil.recursiveDelete(mVenvDir);
     }
 
     @Test
@@ -216,6 +225,71 @@
     }
 
     @Test
+    public void testRun_shouldActivateVenvAndCleanUp_whenVenvIsSet() throws Exception {
+        Mockito.when(mMockBuildInfo.getFile(eq("VIRTUAL_ENV"))).thenReturn(mVenvDir);
+        OptionSetter setter = new OptionSetter(mSpyTest);
+        setter.setOptionValue("python-binaries", mMoblyBinary.getAbsolutePath());
+        File testResult = new File(mSpyTest.getLogDirAbsolutePath(), TEST_RESULT_FILE_NAME);
+        Mockito.when(
+                        mMockRunUtil.runTimedCmd(
+                                anyLong(),
+                                anyString(),
+                                eq("--"),
+                                contains("--device_serial="),
+                                contains("--log_path=")))
+                .thenAnswer(
+                        new Answer<CommandResult>() {
+                            @Override
+                            public CommandResult answer(InvocationOnMock invocation)
+                                    throws Throwable {
+                                FileUtils.createFile(testResult, "");
+                                FileUtils.createFile(
+                                        new File(mSpyTest.getLogDirAbsolutePath(), "log"),
+                                        "log content");
+                                return new CommandResult(CommandStatus.SUCCESS);
+                            }
+                        });
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        result.setStdout(
+                "Name: pip\nLocation: "
+                        + new File(mVenvDir.getAbsolutePath(), "lib/python3.8/site-packages"));
+        Mockito.when(mMockRunUtil.runTimedCmd(anyLong(), anyString(), eq("show"), eq("pip")))
+                .thenReturn(result);
+
+        mSpyTest.run(Mockito.mock(ITestInvocationListener.class));
+
+        verify(mSpyTest.getRunUtil(), times(1))
+                .setEnvVariable(eq("VIRTUAL_ENV"), eq(mVenvDir.getAbsolutePath()));
+        assertFalse(mVenvDir.exists());
+    }
+
+    @Test
+    public void testRun_shouldNotActivateVenv_whenVenvIsNotSet() throws Exception {
+        FileUtil.recursiveDelete(mVenvDir);
+        OptionSetter setter = new OptionSetter(mSpyTest);
+        setter.setOptionValue("python-binaries", mMoblyBinary.getAbsolutePath());
+        File testResult = new File(mSpyTest.getLogDirAbsolutePath(), TEST_RESULT_FILE_NAME);
+        Mockito.when(mMockRunUtil.runTimedCmd(anyLong(), any()))
+                .thenAnswer(
+                        new Answer<CommandResult>() {
+                            @Override
+                            public CommandResult answer(InvocationOnMock invocation)
+                                    throws Throwable {
+                                FileUtils.createFile(testResult, "");
+                                FileUtils.createFile(
+                                        new File(mSpyTest.getLogDirAbsolutePath(), "log"),
+                                        "log content");
+                                return new CommandResult(CommandStatus.SUCCESS);
+                            }
+                        });
+
+        mSpyTest.run(Mockito.mock(ITestInvocationListener.class));
+
+        verify(mSpyTest.getRunUtil(), never())
+                .setEnvVariable(eq("VIRTUAL_ENV"), eq(mVenvDir.getAbsolutePath()));
+    }
+
+    @Test
     public void testBuildCommandLineArrayWithOutConfig() throws Exception {
         Mockito.doNothing().when(mSpyTest).reportLogs(any(), any());
         Mockito.doReturn(DEVICE_SERIAL).when(mMockDevice).getSerialNumber();
@@ -262,7 +336,11 @@
         Mockito.doNothing().when(mSpyTest).reportLogs(any(), any());
         mMockSummaryInputStream = Mockito.mock(InputStream.class);
         mMockParser = Mockito.mock(MoblyYamlResultParser.class);
-        mSpyTest.processYamlTestResults(mMockSummaryInputStream, mMockParser);
+        mSpyTest.processYamlTestResults(
+                mMockSummaryInputStream,
+                mMockParser,
+                Mockito.mock(ITestInvocationListener.class),
+                "runName");
         verify(mMockParser, times(1)).parse(mMockSummaryInputStream);
     }
 
diff --git a/tests/src/com/android/tradefed/testtype/rust/RustBinaryHostTestTest.java b/tests/src/com/android/tradefed/testtype/rust/RustBinaryHostTestTest.java
index 782eac0..22e3c1a 100644
--- a/tests/src/com/android/tradefed/testtype/rust/RustBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/rust/RustBinaryHostTestTest.java
@@ -81,6 +81,24 @@
         return newCommandResult(CommandStatus.SUCCESS, stderr, stdout);
     }
 
+    // shared with RustBinaryTestTest
+    static String runListOutput(int numTests) {
+        String listOutput = "";
+        for (int i = 1; i <= numTests; i++) {
+            listOutput += "test_case_" + i + ": test\n";
+        }
+        return listOutput + numTests + " tests, 0 benchmarks";
+    }
+
+    // shared with RustBinaryTestTest
+    static String runListOutput(String[] tests) {
+        String listOutput = "";
+        for (String name : tests) {
+            listOutput += name + ": test\n";
+        }
+        return listOutput + tests.length + " tests, 0 benchmarks";
+    }
+
     /** Add mocked call "binary --list" to count the number of tests. */
     private void mockCountTests(File binary, int numOfTest) throws Exception {
         EasyMock.expect(
@@ -88,7 +106,7 @@
                                 EasyMock.anyLong(),
                                 EasyMock.eq(binary.getAbsolutePath()),
                                 EasyMock.eq("--list")))
-                .andReturn(successResult("", numOfTest + " tests, 0 benchmarks"));
+                .andReturn(successResult("", runListOutput(numOfTest)));
     }
 
     /** Add mocked testRunStarted call to the listener. */
@@ -184,7 +202,6 @@
             mockListenerLog(binary);
             CommandResult res = newCommandResult(CommandStatus.EXCEPTION, "Err.", "Exception.");
             mockTestRunExpect(binary, res);
-            mMockListener.testRunFailed((String) EasyMock.anyObject());
             mMockListener.testRunFailed((FailureDescription) EasyMock.anyObject());
             mockTestRunEnded();
             callReplayRunVerify();
@@ -261,7 +278,7 @@
                                     EasyMock.eq("--skip"),
                                     EasyMock.eq("Long"),
                                     EasyMock.eq("--list")))
-                    .andReturn(successResult("", "9 tests, 0 benchmarks"));
+                    .andReturn(successResult("", runListOutput(9)));
             mockListenerStarted(binary, 9);
             mockListenerLog(binary);
             CommandResult res = successResult("", resultCount(6, 1, 2));
@@ -289,24 +306,21 @@
         try {
             OptionSetter setter = new OptionSetter(mTest);
             setter.setOptionValue("test-file", binary.getAbsolutePath());
-            setter.setOptionValue("exclude-filter", "NotMe");
-            setter.setOptionValue("include-filter", "OnlyMe");
+            setter.setOptionValue("exclude-filter", "MyTest#NotMe");
+            setter.setOptionValue("include-filter", "MyTest#OnlyMe");
             setter.setOptionValue("exclude-filter", "Other");
-            setter.setOptionValue("include-filter", "Me2");
             // We always pass the include-filter before exclude-filter strings.
-            // Multiple include filters are accepted but all except the 1st are ignored.
             EasyMock.expect(
                             mMockRunUtil.runTimedCmdSilently(
                                     EasyMock.anyLong(),
                                     EasyMock.eq(binary.getAbsolutePath()),
                                     EasyMock.eq("OnlyMe"),
-                                    EasyMock.eq("Me2"),
                                     EasyMock.eq("--skip"),
                                     EasyMock.eq("NotMe"),
                                     EasyMock.eq("--skip"),
                                     EasyMock.eq("Other"),
                                     EasyMock.eq("--list")))
-                    .andReturn(successResult("", "3 tests, 0 benchmarks"));
+                    .andReturn(successResult("", runListOutput(3)));
             mockListenerStarted(binary, 3);
 
             mockListenerLog(binary);
@@ -316,6 +330,77 @@
                                     EasyMock.anyLong(),
                                     EasyMock.eq(binary.getAbsolutePath()),
                                     EasyMock.eq("OnlyMe"),
+                                    EasyMock.eq("--skip"),
+                                    EasyMock.eq("NotMe"),
+                                    EasyMock.eq("--skip"),
+                                    EasyMock.eq("Other")))
+                    .andReturn(res);
+
+            mockTestRunEnded();
+            callReplayRunVerify();
+        } finally {
+            FileUtil.deleteFile(binary);
+        }
+    }
+
+    /** Test multiple include and exclude filters. */
+    @Test
+    public void testMultipleIncludeExcludeFilter() throws Exception {
+        File binary = FileUtil.createTempFile("rust-dir", "");
+        try {
+            OptionSetter setter = new OptionSetter(mTest);
+            setter.setOptionValue("test-file", binary.getAbsolutePath());
+            setter.setOptionValue("exclude-filter", "NotMe");
+            setter.setOptionValue("include-filter", "MyTest#OnlyMe");
+            setter.setOptionValue("exclude-filter", "MyTest#Other");
+            setter.setOptionValue("include-filter", "Me2");
+            // Multiple include filters are run one by one with --list.
+            String[] selection1 = new String[] {"test1", "test2"};
+            EasyMock.expect(
+                            mMockRunUtil.runTimedCmdSilently(
+                                    EasyMock.anyLong(),
+                                    EasyMock.eq(binary.getAbsolutePath()),
+                                    EasyMock.eq("OnlyMe"),
+                                    EasyMock.eq("--skip"),
+                                    EasyMock.eq("NotMe"),
+                                    EasyMock.eq("--skip"),
+                                    EasyMock.eq("Other"),
+                                    EasyMock.eq("--list")))
+                    .andReturn(successResult("", runListOutput(selection1)));
+            String[] selection2 = new String[] {"test2", "test3", "test4"};
+            EasyMock.expect(
+                            mMockRunUtil.runTimedCmdSilently(
+                                    EasyMock.anyLong(),
+                                    EasyMock.eq(binary.getAbsolutePath()),
+                                    EasyMock.eq("Me2"),
+                                    EasyMock.eq("--skip"),
+                                    EasyMock.eq("NotMe"),
+                                    EasyMock.eq("--skip"),
+                                    EasyMock.eq("Other"),
+                                    EasyMock.eq("--list")))
+                    .andReturn(successResult("", runListOutput(selection2)));
+            // Union of selection1 and selection2 has 4 tests.
+            mockListenerStarted(binary, 4);
+
+            // Multiple include filters are run one by one.
+            mockListenerLog(binary);
+            CommandResult res = successResult("", resultCount(2, 0, 0));
+            EasyMock.expect(
+                            mMockRunUtil.runTimedCmd(
+                                    EasyMock.anyLong(),
+                                    EasyMock.eq(binary.getAbsolutePath()),
+                                    EasyMock.eq("OnlyMe"),
+                                    EasyMock.eq("--skip"),
+                                    EasyMock.eq("NotMe"),
+                                    EasyMock.eq("--skip"),
+                                    EasyMock.eq("Other")))
+                    .andReturn(res);
+            mockListenerLog(binary);
+            res = successResult("", resultCount(3, 0, 0));
+            EasyMock.expect(
+                            mMockRunUtil.runTimedCmd(
+                                    EasyMock.anyLong(),
+                                    EasyMock.eq(binary.getAbsolutePath()),
                                     EasyMock.eq("Me2"),
                                     EasyMock.eq("--skip"),
                                     EasyMock.eq("NotMe"),
diff --git a/tests/src/com/android/tradefed/testtype/rust/RustBinaryTestTest.java b/tests/src/com/android/tradefed/testtype/rust/RustBinaryTestTest.java
index a633ed1..3d7d450 100644
--- a/tests/src/com/android/tradefed/testtype/rust/RustBinaryTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/rust/RustBinaryTestTest.java
@@ -28,9 +28,7 @@
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.testtype.coverage.CoverageOptions;
-import com.android.tradefed.util.FileUtil;
 
 import org.easymock.EasyMock;
 import org.junit.Before;
@@ -38,10 +36,7 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-import java.io.File;
-import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.List;
 import java.util.concurrent.TimeUnit;
 
 /** Unit tests for {@link RustBinaryTest}. */
@@ -97,6 +92,14 @@
         EasyMock.verify(mMockInvocationListener, mMockITestDevice, mMockReceiver);
     }
 
+    private String runListOutput(int numTests) {
+        return RustBinaryHostTestTest.runListOutput(numTests);
+    }
+
+    private String runListOutput(String[] tests) {
+        return RustBinaryHostTestTest.runListOutput(tests);
+    }
+
     /** Add mocked Call "path --list" to count the number of tests. */
     private void mockCountTests(String path, String result) throws DeviceNotAvailableException {
         EasyMock.expect(mMockITestDevice.executeShellCommand(path + " --list")).andReturn(result);
@@ -218,12 +221,12 @@
         EasyMock.expect(mMockITestDevice.isDirectory(testPath2)).andReturn(false);
         EasyMock.expect(mMockITestDevice.isExecutable(testPath2)).andReturn(true);
 
-        mockCountTests(testPath1, "test1\n3 tests, 0 benchmarks\n");
+        mockCountTests(testPath1, runListOutput(3));
         mockTestRunStarted("test1", 3);
         mockShellCommand(test1);
         mockTestRunEnded();
 
-        mockCountTests(testPath2, "test2\n7 tests, 0 benchmarks\n");
+        mockCountTests(testPath2, runListOutput(7));
         mockTestRunStarted("test2", 7);
         mockShellCommand(test2);
         mockTestRunEnded();
@@ -247,7 +250,7 @@
         EasyMock.expect(mMockITestDevice.isDirectory(modulePath)).andReturn(false);
         EasyMock.expect(mMockITestDevice.isExecutable(modulePath)).andReturn(true);
 
-        mockCountTests(modulePath, "moduleTest\n1 test, 0 benchmarks\n");
+        mockCountTests(modulePath, runListOutput(1));
         mockTestRunStarted("test1", 1);
         mockShellCommand(modulePath);
         mockTestRunEnded();
@@ -281,216 +284,20 @@
         String[] files2 = new String[] {"test1"};
         EasyMock.expect(mMockITestDevice.getChildren(subDirPath)).andReturn(files2);
 
-        mockCountTests(test1Path, "test1\n5 tests, 0 benchmarks\n");
+        mockCountTests(test1Path, runListOutput(5));
         mockTestRunStarted("test1", 5);
         mockShellCommand(test1Path);
         mockTestRunEnded();
         callReplayRunVerify();
     }
 
-    /** Test cross-process coverage dump for all native processes */
-    @Test
-    public void testNativeCoverageAllProcesses() throws Exception {
-        mCoverageOptionsSetter.setOptionValue("coverage", "true");
-        mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "GCOV");
-        mCoverageOptionsSetter.setOptionValue("coverage-flush", "true");
-
-        final String testPath = RustBinaryTest.DEFAULT_TEST_PATH;
-        final String test1 = "test1";
-        final String test2 = "test2";
-        final String testPath1 = String.format("%s/%s", testPath, test1);
-        final String testPath2 = String.format("%s/%s", testPath, test2);
-        final String coverageTarPath = "/data/misc/trace/coverage.tar";
-
-        MockFileUtil.setMockDirContents(mMockITestDevice, testPath, test1, test2);
-        EasyMock.expect(mMockITestDevice.enableAdbRoot()).andReturn(true);
-        EasyMock.expect(mMockITestDevice.enableAdbRoot()).andReturn(true);
-        EasyMock.expect(mMockITestDevice.executeShellCommand("mkdir /data/misc/trace/testcoverage"))
-                .andReturn("");
-        EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
-        EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
-        EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 -1")).andReturn("");
-        EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 -1")).andReturn("");
-        EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 -1")).andReturn("");
-        // Wait up to 5 minutes for the device to be available after flushing coverage data.
-        mMockITestDevice.waitForDeviceAvailable(5 * 60 * 1000);
-        mMockITestDevice.waitForDeviceAvailable(5 * 60 * 1000);
-        mMockITestDevice.waitForDeviceAvailable(5 * 60 * 1000);
-        EasyMock.expect(mMockITestDevice.executeShellCommand("rm -rf /data/misc/trace/*"))
-                .andReturn("");
-        EasyMock.expect(mMockITestDevice.doesFileExist(testPath)).andReturn(true);
-        EasyMock.expect(mMockITestDevice.isDirectory(testPath)).andReturn(true);
-        EasyMock.expect(mMockITestDevice.isDirectory(testPath1)).andReturn(false);
-        // report the file as executable
-        EasyMock.expect(mMockITestDevice.isExecutable(testPath1)).andReturn(true);
-        EasyMock.expect(mMockITestDevice.isDirectory(testPath2)).andReturn(false);
-        // report the file as executable
-        EasyMock.expect(mMockITestDevice.isExecutable(testPath2)).andReturn(true);
-        EasyMock.expect(
-                        mMockITestDevice.executeShellCommand(
-                                "find /data/misc/trace -name '*.gcda' | tar -cvf"
-                                        + " /data/misc/trace/coverage.tar -T -"))
-                .andReturn("");
-        EasyMock.expect(
-                        mMockITestDevice.executeShellCommand(
-                                "find /data/misc/trace -name '*.gcda' | tar -cvf"
-                                        + " /data/misc/trace/coverage.tar -T -"))
-                .andReturn("");
-        File tmpFile1 = FileUtil.createTempFile("coverage", ".tar");
-        EasyMock.expect(mMockITestDevice.pullFile(coverageTarPath)).andReturn(tmpFile1);
-        File tmpFile2 = FileUtil.createTempFile("coverage", ".tar");
-        EasyMock.expect(mMockITestDevice.pullFile(coverageTarPath)).andReturn(tmpFile2);
-        mMockITestDevice.deleteFile(coverageTarPath);
-        mMockITestDevice.deleteFile(coverageTarPath);
-        mMockInvocationListener.testLog(
-                EasyMock.eq("null_native_runtime_coverage"),
-                EasyMock.eq(LogDataType.NATIVE_COVERAGE),
-                EasyMock.anyObject());
-        mMockInvocationListener.testLog(
-                EasyMock.eq("null_native_runtime_coverage"),
-                EasyMock.eq(LogDataType.NATIVE_COVERAGE),
-                EasyMock.anyObject());
-        EasyMock.expect(
-                        mMockITestDevice.executeShellCommand(
-                                "find /data/misc/trace -name '*.gcda' -delete"))
-                .andReturn("");
-        EasyMock.expect(
-                        mMockITestDevice.executeShellCommand(
-                                "find /data/misc/trace -name '*.gcda' -delete"))
-                .andReturn("");
-        EasyMock.expect(mMockITestDevice.enableAdbRoot()).andReturn(true);
-        EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
-
-        String[] files = new String[] {"test1", "test2"};
-        EasyMock.expect(mMockITestDevice.getChildren(testPath)).andReturn(files);
-
-        mockCountTests(
-                "GCOV_PREFIX=/data/misc/trace/testcoverage " + testPath1,
-                "test1\n1 test, 0 benchmarks\n");
-        mockTestRunStarted("test1", 1);
-        mockShellCommand(test1);
-        mockTestRunEnded();
-        mockCountTests(
-                "GCOV_PREFIX=/data/misc/trace/testcoverage " + testPath2,
-                "test2\n1 test, 0 benchmarks\n");
-        mockTestRunStarted("test2", 1);
-        mockShellCommand(test2);
-        mockTestRunEnded();
-        callReplayRunVerify();
-    }
-
-    /** Test cross-process coverage dump for specific processes */
-    @Test
-    public void testNativeCoverageSpecificProcesses() throws Exception {
-        final List<String> processNames = new ArrayList<>();
-        processNames.add("init");
-        processNames.add("surfaceflinger");
-
-        mCoverageOptionsSetter.setOptionValue("coverage", "true");
-        mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "GCOV");
-        mCoverageOptionsSetter.setOptionValue("coverage-flush", "true");
-        for (String processName : processNames) {
-            mCoverageOptionsSetter.setOptionValue("coverage-processes", processName);
-        }
-
-        final String testPath = RustBinaryTest.DEFAULT_TEST_PATH;
-        final String test1 = "test1";
-        final String test2 = "test2";
-        final String testPath1 = String.format("%s/%s", testPath, test1);
-        final String testPath2 = String.format("%s/%s", testPath, test2);
-        final String coverageTarPath = "/data/misc/trace/coverage.tar";
-
-        MockFileUtil.setMockDirContents(mMockITestDevice, testPath, test1, test2);
-        EasyMock.expect(mMockITestDevice.enableAdbRoot()).andReturn(true);
-        EasyMock.expect(mMockITestDevice.enableAdbRoot()).andReturn(true);
-        EasyMock.expect(mMockITestDevice.executeShellCommand("mkdir /data/misc/trace/testcoverage"))
-                .andReturn("");
-        // Get the pids to flush coverage data.
-        EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
-        EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
-        EasyMock.expect(mMockITestDevice.getProcessPid(processNames.get(0))).andReturn("1");
-        EasyMock.expect(mMockITestDevice.getProcessPid(processNames.get(0))).andReturn("1");
-        EasyMock.expect(mMockITestDevice.getProcessPid(processNames.get(0))).andReturn("1");
-        EasyMock.expect(mMockITestDevice.getProcessPid(processNames.get(1))).andReturn("1000");
-        EasyMock.expect(mMockITestDevice.getProcessPid(processNames.get(1))).andReturn("1000");
-        EasyMock.expect(mMockITestDevice.getProcessPid(processNames.get(1))).andReturn("1000");
-        EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 1 1000")).andReturn("");
-        EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 1 1000")).andReturn("");
-        EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 1 1000")).andReturn("");
-        // Wait up to 5 minutes for the device to be available after flushing coverage data.
-        mMockITestDevice.waitForDeviceAvailable(5 * 60 * 1000);
-        mMockITestDevice.waitForDeviceAvailable(5 * 60 * 1000);
-        mMockITestDevice.waitForDeviceAvailable(5 * 60 * 1000);
-        EasyMock.expect(mMockITestDevice.executeShellCommand("rm -rf /data/misc/trace/*"))
-                .andReturn("");
-        EasyMock.expect(mMockITestDevice.doesFileExist(testPath)).andReturn(true);
-        EasyMock.expect(mMockITestDevice.isDirectory(testPath)).andReturn(true);
-        EasyMock.expect(mMockITestDevice.isDirectory(testPath1)).andReturn(false);
-        // report the file as executable
-        EasyMock.expect(mMockITestDevice.isExecutable(testPath1)).andReturn(true);
-        EasyMock.expect(mMockITestDevice.isDirectory(testPath2)).andReturn(false);
-        // report the file as executable
-        EasyMock.expect(mMockITestDevice.isExecutable(testPath2)).andReturn(true);
-        EasyMock.expect(
-                        mMockITestDevice.executeShellCommand(
-                                "find /data/misc/trace -name '*.gcda' | tar -cvf"
-                                        + " /data/misc/trace/coverage.tar -T -"))
-                .andReturn("");
-        EasyMock.expect(
-                        mMockITestDevice.executeShellCommand(
-                                "find /data/misc/trace -name '*.gcda' | tar -cvf"
-                                        + " /data/misc/trace/coverage.tar -T -"))
-                .andReturn("");
-        File tmpFile1 = FileUtil.createTempFile("coverage", ".tar");
-        EasyMock.expect(mMockITestDevice.pullFile(coverageTarPath)).andReturn(tmpFile1);
-        File tmpFile2 = FileUtil.createTempFile("coverage", ".tar");
-        EasyMock.expect(mMockITestDevice.pullFile(coverageTarPath)).andReturn(tmpFile2);
-        mMockITestDevice.deleteFile(coverageTarPath);
-        mMockITestDevice.deleteFile(coverageTarPath);
-        mMockInvocationListener.testLog(
-                EasyMock.eq("null_native_runtime_coverage"),
-                EasyMock.eq(LogDataType.NATIVE_COVERAGE),
-                EasyMock.anyObject());
-        mMockInvocationListener.testLog(
-                EasyMock.eq("null_native_runtime_coverage"),
-                EasyMock.eq(LogDataType.NATIVE_COVERAGE),
-                EasyMock.anyObject());
-        EasyMock.expect(
-                        mMockITestDevice.executeShellCommand(
-                                "find /data/misc/trace -name '*.gcda' -delete"))
-                .andReturn("");
-        EasyMock.expect(
-                        mMockITestDevice.executeShellCommand(
-                                "find /data/misc/trace -name '*.gcda' -delete"))
-                .andReturn("");
-        EasyMock.expect(mMockITestDevice.enableAdbRoot()).andReturn(true);
-        EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
-
-        String[] files = new String[] {"test1", "test2"};
-        EasyMock.expect(mMockITestDevice.getChildren(testPath)).andReturn(files);
-
-        mockCountTests(
-                "GCOV_PREFIX=/data/misc/trace/testcoverage " + testPath1,
-                "test1\n1 test, 0 benchmarks\n");
-        mockTestRunStarted("test1", 1);
-        mockShellCommand(test1);
-        mockTestRunEnded();
-        mockCountTests(
-                "GCOV_PREFIX=/data/misc/trace/testcoverage " + testPath2,
-                "test2\n1 test, 0 benchmarks\n");
-        mockTestRunStarted("test2", 1);
-        mockShellCommand(test2);
-        mockTestRunEnded();
-        callReplayRunVerify();
-    }
-
     /**
      * Helper function to do the actual filtering test.
      *
      * @param filterString The string to search for in the Mock, to verify filtering was called
      * @throws DeviceNotAvailableException
      */
-    private void doTestFilter(String filterString) throws DeviceNotAvailableException {
+    private void doTestFilter(String[] filterStrings) throws DeviceNotAvailableException {
         final String testPath = RustBinaryTest.DEFAULT_TEST_PATH;
         final String test1 = "test1";
         final String testPath1 = String.format("%s/%s", testPath, test1);
@@ -504,9 +311,13 @@
         EasyMock.expect(mMockITestDevice.isDirectory(testPath1)).andReturn(false);
         EasyMock.expect(mMockITestDevice.isExecutable(testPath1)).andReturn(true);
 
-        mockCountTests(testPath1 + filterString, "test1\n3 tests, 0 benchmarks\n");
+        for (String filter : filterStrings) {
+            mockCountTests(testPath1 + filter, runListOutput(3));
+        }
         mockTestRunStarted("test1", 3);
-        mockShellCommand(test1 + filterString);
+        for (String filter : filterStrings) {
+            mockShellCommand(test1 + filter);
+        }
         mockTestRunEnded();
         callReplayRunVerify();
     }
@@ -516,8 +327,8 @@
     public void testExcludeFilter() throws Exception {
         OptionSetter setter = new OptionSetter(mRustBinaryTest);
         setter.setOptionValue("exclude-filter", "NotMe");
-        setter.setOptionValue("exclude-filter", "Long");
-        doTestFilter(" --skip NotMe --skip Long");
+        setter.setOptionValue("exclude-filter", "MyTest#Long");
+        doTestFilter(new String[] {" --skip NotMe --skip Long"});
     }
 
     /** Test both include- and exclude-filter options. */
@@ -525,11 +336,24 @@
     public void testIncludeExcludeFilter() throws Exception {
         OptionSetter setter = new OptionSetter(mRustBinaryTest);
         setter.setOptionValue("exclude-filter", "NotMe2");
-        setter.setOptionValue("include-filter", "OnlyMe");
+        setter.setOptionValue("include-filter", "MyTest#OnlyMe");
+        setter.setOptionValue("exclude-filter", "MyTest#other");
+        // Include filters are passed before exclude filters.
+        doTestFilter(new String[] {" OnlyMe --skip NotMe2 --skip other"});
+    }
+
+    /** Test multiple include- and exclude-filter options. */
+    @Test
+    public void testMultipleIncludeExcludeFilter() throws Exception {
+        OptionSetter setter = new OptionSetter(mRustBinaryTest);
+        setter.setOptionValue("exclude-filter", "MyTest#NotMe2");
+        setter.setOptionValue("include-filter", "MyTest#OnlyMe");
         setter.setOptionValue("exclude-filter", "other");
         setter.setOptionValue("include-filter", "Me2");
-        // Include filters are passed before exclude filters.
-        // Multiple include filters are accepted, but all except the 1st are ignored.
-        doTestFilter(" OnlyMe Me2 --skip NotMe2 --skip other");
+        // Multiple include filters are run one by one.
+        doTestFilter(
+                new String[] {
+                    " OnlyMe --skip NotMe2 --skip other", " Me2 --skip NotMe2 --skip other"
+                });
     }
 }
diff --git a/tests/src/com/android/tradefed/testtype/rust/RustTestBaseTest.java b/tests/src/com/android/tradefed/testtype/rust/RustTestBaseTest.java
index a7a1770..137434b 100644
--- a/tests/src/com/android/tradefed/testtype/rust/RustTestBaseTest.java
+++ b/tests/src/com/android/tradefed/testtype/rust/RustTestBaseTest.java
@@ -15,10 +15,7 @@
  */
 package com.android.tradefed.testtype.rust;
 
-import static com.google.common.truth.Truth.assertThat;
-
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
 
 import com.android.tradefed.testtype.ITestFilterReceiver;
 
@@ -26,7 +23,6 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-import java.text.ParseException;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
@@ -79,47 +75,4 @@
         runner.clearExcludeFilters();
         assertEquals(set0, runner.getExcludeFilters());
     }
-
-    /** Test parsing of various forms of Rust test list summary output. */
-    @Test
-    public void testParseTestListSummaries() throws Exception {
-        assertEquals(0, RustTestBase.parseTestListCount(new String[] {"0 tests, 0 benchmarks"}));
-        assertEquals(1, RustTestBase.parseTestListCount(new String[] {"1 test, 0 benchmarks"}));
-        assertEquals(0, RustTestBase.parseTestListCount(new String[] {"0 tests, 1 benchmark"}));
-        assertEquals(2, RustTestBase.parseTestListCount(new String[] {"2 tests, 0 benchmarks"}));
-        assertEquals(2, RustTestBase.parseTestListCount(new String[] {"2 tests, 1 benchmark"}));
-        assertEquals(2, RustTestBase.parseTestListCount(new String[] {"2 tests, 2 benchmarks"}));
-    }
-
-    /** Test parsing of a real Rust test list. */
-    @Test
-    public void testParseTestListRealOutput() throws Exception {
-        String[] contents = readInFile(RUST_LIST_FILE_1);
-        assertEquals(42, RustTestBase.parseTestListCount(contents));
-    }
-
-    /** Test parsing of an empty Rust test list. */
-    @Test
-    public void testParseTestListEmpty() throws Exception {
-        try {
-            RustTestBase.parseTestListCount(new String[] {});
-            fail("Should have thrown an exception.");
-        } catch (ParseException e) {
-            assertThat(e).hasMessageThat().contains("Test did not return any output");
-        }
-    }
-
-    /** Test parsing of a malformed or non-standard Rust test list. */
-    @Test
-    public void testParseTestListMalformed() throws Exception {
-        String[] contents = {
-            "some other output", "that does not contain a summary line",
-        };
-        try {
-            RustTestBase.parseTestListCount(contents);
-            fail("Should have thrown an exception.");
-        } catch (ParseException e) {
-            assertThat(e).hasMessageThat().contains("Could not match total");
-        }
-    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/rust/RustTestResultParserTest.java b/tests/src/com/android/tradefed/testtype/rust/RustTestResultParserTest.java
index d5ed861..af6bafd 100644
--- a/tests/src/com/android/tradefed/testtype/rust/RustTestResultParserTest.java
+++ b/tests/src/com/android/tradefed/testtype/rust/RustTestResultParserTest.java
@@ -32,6 +32,7 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.util.Arrays;
 import java.util.HashMap;
 
 /** Unit tests for {@link RustTestResultParser}. */
@@ -108,6 +109,7 @@
 
         replay(mMockListener);
         mParser.processNewLines(contents);
+        mParser.done();
         verify(mMockListener);
     }
 
@@ -126,6 +128,7 @@
                 (String) EasyMock.anyObject());
         replay(mMockListener);
         mParser.processNewLines(contents);
+        mParser.done();
         verify(mMockListener);
     }
 
@@ -141,6 +144,27 @@
                 EasyMock.eq(new TestDescription("test", "make_sure_no_proc_macro")));
         replay(mMockListener);
         mParser.processNewLines(contents);
+        mParser.done();
+        verify(mMockListener);
+    }
+
+    /**
+     * Tests may not return all their output in a single call to processNewLines. This tests that we
+     * properly parse output split across several calls.
+     */
+    @Test
+    public void testParsePartialOutput() {
+        String[] contents = readInFile(RUST_OUTPUT_FILE_1);
+        for (int i = 0; i < 10; i++) {
+            mMockListener.testStarted(EasyMock.anyObject());
+            mMockListener.testEnded(
+                    EasyMock.anyObject(), EasyMock.<HashMap<String, Metric>>anyObject());
+        }
+        replay(mMockListener);
+        mParser.processNewLines(Arrays.copyOfRange(contents, 0, 4));
+        mParser.processNewLines(Arrays.copyOfRange(contents, 4, 7));
+        mParser.processNewLines(Arrays.copyOfRange(contents, 4, contents.length));
+        mParser.done();
         verify(mMockListener);
     }
 }
diff --git a/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java b/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
index 3cee111..1050460 100644
--- a/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
@@ -26,6 +26,7 @@
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.cloud.RemoteAndroidVirtualDevice;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.DeviceUnresponsiveException;
 import com.android.tradefed.device.ITestDevice;
@@ -263,13 +264,22 @@
 
     private GranularRetriableTestWrapper createGranularTestWrapper(
             IRemoteTest test, int maxRunCount) throws Exception {
-        return createGranularTestWrapper(test, maxRunCount, new ArrayList<>());
+        return createGranularTestWrapper(test, maxRunCount, new ArrayList<>(), null);
     }
 
     private GranularRetriableTestWrapper createGranularTestWrapper(
             IRemoteTest test, int maxRunCount, List<IMetricCollector> collectors) throws Exception {
+        return createGranularTestWrapper(test, maxRunCount, collectors, null);
+    }
+
+    private GranularRetriableTestWrapper createGranularTestWrapper(
+            IRemoteTest test,
+            int maxRunCount,
+            List<IMetricCollector> collectors,
+            ModuleDefinition module)
+            throws Exception {
         GranularRetriableTestWrapper granularTestWrapper =
-                new GranularRetriableTestWrapper(test, null, null, null, maxRunCount);
+                new GranularRetriableTestWrapper(test, module, null, null, null, maxRunCount);
         granularTestWrapper.setModuleId("test module");
         granularTestWrapper.setMarkTestsSkipped(false);
         granularTestWrapper.setMetricCollectors(collectors);
@@ -904,6 +914,101 @@
         EasyMock.verify(mMockDevice, mMockDevice2);
     }
 
+    /** Test to reset multi-devices at the last intra-module retry. */
+    @Test
+    public void testIntraModuleRun_resetMultiDevicesAtLastIntraModuleRetry() throws Exception {
+        IRetryDecision decision = new BaseRetryDecision();
+        OptionSetter setter = new OptionSetter(decision);
+        setter.setOptionValue("reset-at-last-retry", "true");
+        setter.setOptionValue("retry-strategy", "RETRY_ANY_FAILURE");
+        setter.setOptionValue("max-testcase-run-count", Integer.toString(3));
+        decision.setInvocationContext(mModuleInvocationContext);
+        FakeTest test = new FakeTest();
+        test.setRunFailure("I failed!");
+        ITestDevice noneAVDDevice = EasyMock.createMock(ITestDevice.class);
+
+        RemoteAndroidVirtualDevice avdDevice = Mockito.mock(RemoteAndroidVirtualDevice.class);
+        Mockito.when(avdDevice.powerwashGce()).thenReturn(true);
+
+        ModuleDefinition module = Mockito.mock(ModuleDefinition.class);
+        // Should call suite level preparers.
+        Mockito.when(module.runPreparation(true)).thenReturn(null);
+
+        mModuleInvocationContext.addAllocatedDevice("default-device1", noneAVDDevice);
+        mModuleInvocationContext.addAllocatedDevice("default-device2", avdDevice);
+        GranularRetriableTestWrapper granularTestWrapper =
+                createGranularTestWrapper(test, 3, new ArrayList<>(), module);
+        granularTestWrapper.setRetryDecision(decision);
+        EasyMock.expect(noneAVDDevice.getIDevice())
+                .andStubReturn(EasyMock.createMock(IDevice.class));
+        EasyMock.expect(noneAVDDevice.getSerialNumber()).andStubReturn("device-1");
+
+        EasyMock.replay(noneAVDDevice);
+        granularTestWrapper.run(mModuleInfo, new CollectingTestListener());
+        EasyMock.verify(noneAVDDevice);
+    }
+
+    /** Test to reset device at the last intra-module retry failed due to preparer failure. */
+    @Test
+    public void testIntraModuleRun_resetFailed_preparerFailure() throws Exception {
+        IRetryDecision decision = new BaseRetryDecision();
+        OptionSetter setter = new OptionSetter(decision);
+        setter.setOptionValue("reset-at-last-retry", "true");
+        setter.setOptionValue("retry-strategy", "RETRY_ANY_FAILURE");
+        setter.setOptionValue("max-testcase-run-count", Integer.toString(3));
+        decision.setInvocationContext(mModuleInvocationContext);
+        FakeTest test = new FakeTest();
+        test.setRunFailure("I failed!");
+
+        RemoteAndroidVirtualDevice avdDevice = Mockito.mock(RemoteAndroidVirtualDevice.class);
+        Mockito.when(avdDevice.powerwashGce()).thenReturn(true);
+
+        ModuleDefinition module = Mockito.mock(ModuleDefinition.class);
+        // Suite level preparers failed.
+        Mockito.when(module.runPreparation(true)).thenReturn(new RuntimeException());
+
+        mModuleInvocationContext.addAllocatedDevice("default-device2", avdDevice);
+        GranularRetriableTestWrapper granularTestWrapper =
+                createGranularTestWrapper(test, 3, new ArrayList<>(), module);
+        granularTestWrapper.setRetryDecision(decision);
+
+        try {
+            granularTestWrapper.run(mModuleInfo, new CollectingTestListener());
+            fail("Exception should be raised when reset is failed.");
+        } catch (DeviceNotAvailableException e) {
+            assertTrue(e.getMessage().startsWith("Failed to reset devices before retry: "));
+        }
+    }
+
+    /** Test to reset device at the last intra-module retry failed due to reset failure. */
+    @Test
+    public void testIntraModuleRun_resetFailed_powerwashFailure() throws Exception {
+        IRetryDecision decision = new BaseRetryDecision();
+        OptionSetter setter = new OptionSetter(decision);
+        setter.setOptionValue("reset-at-last-retry", "true");
+        setter.setOptionValue("retry-strategy", "RETRY_ANY_FAILURE");
+        setter.setOptionValue("max-testcase-run-count", Integer.toString(3));
+        decision.setInvocationContext(mModuleInvocationContext);
+        FakeTest test = new FakeTest();
+        test.setRunFailure("I failed!");
+
+        RemoteAndroidVirtualDevice device = Mockito.mock(RemoteAndroidVirtualDevice.class);
+        Mockito.when(device.powerwashGce()).thenReturn(false);
+        Mockito.when(device.getSerialNumber()).thenReturn("device1");
+
+        test.setDevice(device);
+        mModuleInvocationContext.addAllocatedDevice("default-device1", device);
+        GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(test, 3);
+        granularTestWrapper.setRetryDecision(decision);
+
+        try {
+            granularTestWrapper.run(mModuleInfo, new CollectingTestListener());
+            fail("Exception should be raised when reset is failed.");
+        } catch (DeviceNotAvailableException e) {
+            assertEquals("Failed to powerwash device: device1", e.getMessage());
+        }
+    }
+
     /** Collector that track if it was called or not */
     public static class CalledMetricCollector extends BaseDeviceMetricCollector {
 
diff --git a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
index 592a71d..876ba74 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
@@ -59,6 +59,7 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.MultiFailureDescription;
 import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.retry.BaseRetryDecision;
 import com.android.tradefed.retry.IRetryDecision;
@@ -227,7 +228,8 @@
                 TestDescription test = new TestDescription(EMPTY_CONFIG, EMPTY_CONFIG);
                 listener.testStarted(test, 0);
                 if (mFailed != null) {
-                    listener.testFailed(test, mFailed);
+                    listener.testFailed(
+                            test, FailureDescription.create(mFailed, FailureStatus.TEST_FAILURE));
                 }
                 listener.testEnded(test, 5, new HashMap<String, Metric>());
             } finally {
@@ -399,7 +401,8 @@
         TestDescription test = new TestDescription(EMPTY_CONFIG, EMPTY_CONFIG);
         listener.testStarted(test, 0);
         if (testFailed) {
-            listener.testFailed(test, message);
+            listener.testFailed(
+                    test, FailureDescription.create(message, FailureStatus.TEST_FAILURE));
         }
         listener.testEnded(test, 5, new HashMap<String, Metric>());
         listener.testRunEnded(EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
@@ -415,7 +418,9 @@
             TestDescription test = new TestDescription(EMPTY_CONFIG, EMPTY_CONFIG);
             listener.testStarted(test, 0);
             if (testFailed) {
-                listener.testFailed(test, mTestFailedMessage);
+                listener.testFailed(
+                        test,
+                        FailureDescription.create(mTestFailedMessage, FailureStatus.TEST_FAILURE));
             }
             listener.testEnded(test, 5, new HashMap<String, Metric>());
             listener.testRunEnded(
@@ -661,7 +666,9 @@
                             fake.setTest(
                                     new StubCollectingTest(
                                             new DeviceUnresponsiveException(
-                                                    "unresponsive", "serial")));
+                                                    "unresponsive",
+                                                    "serial",
+                                                    DeviceErrorIdentifier.DEVICE_UNRESPONSIVE)));
                             testConfig.put(TEST_CONFIG_NAME, fake);
                         } catch (ConfigurationException e) {
                             CLog.e(e);
@@ -683,7 +690,8 @@
         mMockListener.testRunStarted(
                 EasyMock.eq(TEST_CONFIG_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         EasyMock.expectLastCall().times(1);
-        mMockListener.testRunFailed(FailureDescription.create("unresponsive"));
+        mMockListener.testRunFailed(
+                FailureDescription.create("unresponsive", FailureStatus.LOST_SYSTEM_UNDER_TEST));
         EasyMock.expect(
                         mMockDevice.logBugreport(
                                 EasyMock.eq("module-test-failure-SERIAL-bugreport"),
@@ -811,11 +819,6 @@
         EasyMock.expectLastCall().times(1);
         Capture<FailureDescription> captured = new Capture<>();
         mMockListener.testRunFailed(EasyMock.capture(captured));
-        EasyMock.expect(
-                        mMockDevice.logBugreport(
-                                EasyMock.eq("module-test-failure-SERIAL-bugreport"),
-                                EasyMock.anyObject()))
-                .andReturn(true);
         mMockListener.testRunEnded(
                 EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
         EasyMock.expectLastCall().times(1);
@@ -1604,7 +1607,9 @@
                     EasyMock.anyLong());
             TestDescription testId = new TestDescription(EMPTY_CONFIG, EMPTY_CONFIG);
             mMockListener.testStarted(testId, 0);
-            mMockListener.testFailed(testId, mTestFailedMessage);
+            mMockListener.testFailed(
+                    testId,
+                    FailureDescription.create(mTestFailedMessage, FailureStatus.TEST_FAILURE));
             mMockListener.testEnded(testId, 5, new HashMap<String, Metric>());
             mMockListener.testRunEnded(
                     EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
diff --git a/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java b/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
index 18e3ce8..cc0819d 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
@@ -38,7 +38,6 @@
 import com.android.tradefed.device.DeviceUnresponsiveException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.ITestDevice.RecoveryMode;
-import com.android.tradefed.device.StubDevice;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.invoker.TestInvocation;
@@ -57,6 +56,8 @@
 import com.android.tradefed.result.ResultForwarder;
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.retry.BaseRetryDecision;
 import com.android.tradefed.retry.IRetryDecision;
 import com.android.tradefed.targetprep.BaseTargetPreparer;
@@ -160,10 +161,12 @@
                 TestDescription test = new TestDescription(mRunName + "class", "test" + i);
                 listener.testStarted(test);
                 if (mShouldThrow && i == mNumTest / 2) {
-                    throw new DeviceNotAvailableException("unavailable", "serial");
+                    throw new DeviceNotAvailableException(
+                            "unavailable", "serial", DeviceErrorIdentifier.DEVICE_UNAVAILABLE);
                 }
                 if (mDeviceUnresponsive) {
-                    throw new DeviceUnresponsiveException("unresponsive", "serial");
+                    throw new DeviceUnresponsiveException(
+                            "unresponsive", "serial", DeviceErrorIdentifier.DEVICE_UNRESPONSIVE);
                 }
                 if (mThrowError && i == mNumTest / 2) {
                     throw new AssertionError("assert error");
@@ -238,7 +241,9 @@
                         continue;
                     }
                     listener.testStarted(test);
-                    listener.testFailed(test, "I failed.");
+                    listener.testFailed(
+                            test,
+                            FailureDescription.create("I failed.", FailureStatus.TEST_FAILURE));
                     listener.testEnded(test, new HashMap<String, Metric>());
                 }
                 listener.testRunEnded(0, new HashMap<String, Metric>());
@@ -539,7 +544,7 @@
                     EasyMock.anyLong(),
                     EasyMock.<HashMap<String, Metric>>anyObject());
         }
-        mMockListener.testFailed(EasyMock.anyObject(), (String) EasyMock.anyObject());
+        mMockListener.testFailed(EasyMock.anyObject(), (FailureDescription) EasyMock.anyObject());
         Capture<FailureDescription> captured = new Capture<>();
         mMockListener.testRunFailed(EasyMock.capture(captured));
         mMockListener.testRunEnded(
@@ -981,7 +986,7 @@
                     EasyMock.anyLong(),
                     EasyMock.<HashMap<String, Metric>>anyObject());
         }
-        mMockListener.testFailed(EasyMock.anyObject(), (String) EasyMock.anyObject());
+        mMockListener.testFailed(EasyMock.anyObject(), (FailureDescription) EasyMock.anyObject());
         mMockListener.testRunFailed((FailureDescription) EasyMock.anyObject());
         mMockListener.testRunEnded(
                 EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
@@ -1043,17 +1048,11 @@
                     EasyMock.anyLong(),
                     EasyMock.<HashMap<String, Metric>>anyObject());
         }
-        mMockListener.testFailed(EasyMock.anyObject(), (String) EasyMock.anyObject());
+        mMockListener.testFailed(EasyMock.anyObject(), (FailureDescription) EasyMock.anyObject());
         mMockListener.testRunFailed((FailureDescription) EasyMock.anyObject());
         mMockListener.testRunEnded(
                 EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
 
-        // Run failed
-        EasyMock.expect(mMockDevice.getIDevice()).andReturn(EasyMock.createMock(IDevice.class));
-        EasyMock.expect(mMockDevice.getSerialNumber()).andReturn("serial");
-        EasyMock.expect(mMockDevice.logBugreport(EasyMock.anyObject(), EasyMock.anyObject()))
-                .andReturn(true);
-
         replayMocks();
         mModule.run(mModuleInfo, mMockListener);
         // Only one module
@@ -1120,7 +1119,8 @@
                             throws DeviceNotAvailableException {
                         listener.testRunStarted("test", 1);
                         listener.testFailed(
-                                new TestDescription("failedclass", "failedmethod"), "trace");
+                                new TestDescription("failedclass", "failedmethod"),
+                                FailureDescription.create("trace", FailureStatus.TEST_FAILURE));
                     }
                 });
         mTargetPrepList.clear();
@@ -1168,7 +1168,9 @@
                         TestDescription tid = new TestDescription("class", "method");
                         listener.testRunStarted("test", 1);
                         listener.testStarted(tid);
-                        listener.testFailed(tid, "I failed");
+                        listener.testFailed(
+                                tid,
+                                FailureDescription.create("I failed", FailureStatus.TEST_FAILURE));
                         listener.testEnded(tid, new HashMap<String, Metric>());
                         listener.testRunEnded(0, new HashMap<String, Metric>());
                     }
@@ -1325,7 +1327,8 @@
                     public void run(TestInformation testInfo, ITestInvocationListener listener)
                             throws DeviceNotAvailableException {
                         listener.testFailed(
-                                new TestDescription("failedclass", "failedmethod"), "trace");
+                                new TestDescription("failedclass", "failedmethod"),
+                                FailureDescription.create("trace", FailureStatus.TEST_FAILURE));
                     }
                 });
         mTargetPrepList.clear();
@@ -1387,8 +1390,9 @@
                     EasyMock.anyLong(),
                     EasyMock.<HashMap<String, Metric>>anyObject());
         }
-        mMockListener.testFailed(EasyMock.anyObject(), (String) EasyMock.anyObject());
-        FailureDescription issues = FailureDescription.create("unresponsive");
+        mMockListener.testFailed(EasyMock.anyObject(), (FailureDescription) EasyMock.anyObject());
+        FailureDescription issues =
+                FailureDescription.create("unresponsive", FailureStatus.LOST_SYSTEM_UNDER_TEST);
         mMockListener.testRunFailed(issues);
         mMockListener.testRunEnded(
                 EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
@@ -1564,7 +1568,6 @@
         mModule.setBuild(mMockBuildInfo);
         mModule.setDevice(mMockDevice);
 
-        EasyMock.expect(mMockDevice.getIDevice()).andReturn(new StubDevice("fake"));
         EasyMock.expect(mMockPrep.isDisabled()).andReturn(false).times(2);
         // no isTearDownDisabled() expected for setup
         mMockPrep.setUp(EasyMock.eq(mModuleInfo));
@@ -1635,7 +1638,8 @@
                     EasyMock.<HashMap<String, Metric>>anyObject());
             TestDescription testFail0 = new TestDescription(runName + "0class", "fail0");
             mMockListener.testStarted(EasyMock.eq(testFail0), EasyMock.anyLong());
-            mMockListener.testFailed(EasyMock.eq(testFail0), (String) EasyMock.anyObject());
+            mMockListener.testFailed(
+                    EasyMock.eq(testFail0), (FailureDescription) EasyMock.anyObject());
             mMockListener.testEnded(
                     EasyMock.eq(testFail0),
                     EasyMock.anyLong(),
@@ -1656,7 +1660,8 @@
                     EasyMock.<HashMap<String, Metric>>anyObject());
             TestDescription testFail0_1 = new TestDescription(runName + "1class", "fail0");
             mMockListener.testStarted(EasyMock.eq(testFail0_1), EasyMock.anyLong());
-            mMockListener.testFailed(EasyMock.eq(testFail0_1), (String) EasyMock.anyObject());
+            mMockListener.testFailed(
+                    EasyMock.eq(testFail0_1), (FailureDescription) EasyMock.anyObject());
             mMockListener.testEnded(
                     EasyMock.eq(testFail0_1),
                     EasyMock.anyLong(),
@@ -1741,7 +1746,8 @@
             }
             TestDescription testFail0 = new TestDescription(runName + "0class", "fail0");
             mMockListener.testStarted(EasyMock.eq(testFail0), EasyMock.anyLong());
-            mMockListener.testFailed(EasyMock.eq(testFail0), (String) EasyMock.anyObject());
+            mMockListener.testFailed(
+                    EasyMock.eq(testFail0), (FailureDescription) EasyMock.anyObject());
             mMockListener.testEnded(
                     EasyMock.eq(testFail0),
                     EasyMock.anyLong(),
@@ -1766,7 +1772,8 @@
             }
             TestDescription testFail0_1 = new TestDescription(runName + "1class", "fail0");
             mMockListener.testStarted(EasyMock.eq(testFail0_1), EasyMock.anyLong());
-            mMockListener.testFailed(EasyMock.eq(testFail0_1), (String) EasyMock.anyObject());
+            mMockListener.testFailed(
+                    EasyMock.eq(testFail0_1), (FailureDescription) EasyMock.anyObject());
             mMockListener.testEnded(
                     EasyMock.eq(testFail0_1),
                     EasyMock.anyLong(),
diff --git a/tests/src/com/android/tradefed/testtype/suite/ModuleSplitterTest.java b/tests/src/com/android/tradefed/testtype/suite/ModuleSplitterTest.java
index 2b3db6b..5d43bb2 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ModuleSplitterTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ModuleSplitterTest.java
@@ -37,8 +37,11 @@
 import org.junit.runners.JUnit4;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 /** Unit tests for {@link ModuleSplitter}. */
 @RunWith(JUnit4.class)
@@ -46,6 +49,8 @@
 
     private static final String DEFAULT_DEVICE = ConfigurationDef.DEFAULT_DEVICE_NAME;
     private TestInformation mTestInfo = TestInformation.newBuilder().build();
+    private Map<String, List<ITargetPreparer>> mSuitePreparersPerDevice =
+            new HashMap<String, List<ITargetPreparer>>();
 
     /**
      * Tests that {@link ModuleSplitter#splitConfiguration(TestInformation, LinkedHashMap, int,
@@ -69,7 +74,8 @@
         setter.setOptionValue("not-shardable", "true");
         runConfig.put("module1", config);
         List<ModuleDefinition> res =
-                ModuleSplitter.splitConfiguration(mTestInfo, runConfig, 5, true, true);
+                ModuleSplitter.splitConfiguration(
+                        mTestInfo, runConfig, mSuitePreparersPerDevice, 5, true, true);
         // matching 1 for 1, config to ModuleDefinition since not shardable
         assertEquals(1, res.size());
         // The original target preparer is changed since we split multiple <test> tags.
@@ -105,7 +111,8 @@
         setter.setOptionValue("not-strict-shardable", "true");
         runConfig.put("module1", config);
         List<ModuleDefinition> res =
-                ModuleSplitter.splitConfiguration(mTestInfo, runConfig, 5, true, true);
+                ModuleSplitter.splitConfiguration(
+                        mTestInfo, runConfig, mSuitePreparersPerDevice, 5, true, true);
         // We are sharding since even if we are not-strict-shardable, we are in dynamic context
         assertEquals(10, res.size());
         // The original target preparer is changed since we split multiple <test> tags.
@@ -140,7 +147,8 @@
         setter.setOptionValue("not-strict-shardable", "true");
         runConfig.put("module1", config);
         List<ModuleDefinition> res =
-                ModuleSplitter.splitConfiguration(mTestInfo, runConfig, 5, false, true);
+                ModuleSplitter.splitConfiguration(
+                        mTestInfo, runConfig, mSuitePreparersPerDevice, 5, false, true);
         // matching 1 for 1, config to ModuleDefinition since not shardable
         assertEquals(1, res.size());
         // The original target preparer is changed since we split multiple <test> tags.
@@ -169,7 +177,8 @@
 
         runConfig.put("module1", config);
         List<ModuleDefinition> res =
-                ModuleSplitter.splitConfiguration(mTestInfo, runConfig, 5, true, false);
+                ModuleSplitter.splitConfiguration(
+                        mTestInfo, runConfig, mSuitePreparersPerDevice, 5, true, false);
         // matching 1 for 1, config to ModuleDefinition since no intra-module sharding
         assertEquals(1, res.size());
         // The original target preparer is changed since we split multiple <test> tags.
@@ -206,7 +215,8 @@
 
         runConfig.put("module1", config);
         List<ModuleDefinition> res =
-                ModuleSplitter.splitConfiguration(mTestInfo, runConfig, 5, true, true);
+                ModuleSplitter.splitConfiguration(
+                        mTestInfo, runConfig, mSuitePreparersPerDevice, 5, true, true);
         // matching 1 for 1, config to ModuleDefinition since not shardable
         assertEquals(1, res.size());
         // The original target preparer is not there, it has been copied
@@ -239,7 +249,8 @@
 
         runConfig.put("module1", config);
         List<ModuleDefinition> res =
-                ModuleSplitter.splitConfiguration(mTestInfo, runConfig, 5, true, true);
+                ModuleSplitter.splitConfiguration(
+                        mTestInfo, runConfig, mSuitePreparersPerDevice, 5, true, true);
         // matching 1 for 1, config to ModuleDefinition since did not shard
         assertEquals(1, res.size());
         // The original target preparer is not there, it has been copied
@@ -271,14 +282,25 @@
         setter.setOptionValue("num-shards", "6");
         config.setTest(test);
 
+        Map<String, List<ITargetPreparer>> suitePreparers =
+                new HashMap<String, List<ITargetPreparer>>();
+        ITargetPreparer preparer1 = new StubTargetPreparer();
+        ITargetPreparer preparer2 = new StubTargetPreparer();
+        List<ITargetPreparer> preparers = Arrays.asList(preparer1, preparer2);
+        suitePreparers.put(DEFAULT_DEVICE, preparers);
+
         runConfig.put("module1", config);
         List<ModuleDefinition> res =
-                ModuleSplitter.splitConfiguration(mTestInfo, runConfig, 5, true, true);
+                ModuleSplitter.splitConfiguration(
+                        mTestInfo, runConfig, suitePreparers, 5, true, true);
         // matching 1 for 10 since tests sharding in 5 units times 2.
         assertEquals(10, res.size());
         // The original IRemoteTest does not exists anymore, new IRemoteTests have been created.
         for (ModuleDefinition m : res) {
             assertNotSame(test, m.getTests().get(0));
+            assertEquals(2, m.getSuitePreparerForDevice(DEFAULT_DEVICE).size());
+            assertNotSame(preparer1, m.getSuitePreparerForDevice(DEFAULT_DEVICE).get(0));
+            assertNotSame(preparer1, m.getSuitePreparerForDevice(DEFAULT_DEVICE).get(1));
         }
         assertTrue(config.getTests().isEmpty());
     }
@@ -302,7 +324,8 @@
 
         runConfig.put("module1", config);
         List<ModuleDefinition> res =
-                ModuleSplitter.splitConfiguration(mTestInfo, runConfig, 5, false, true);
+                ModuleSplitter.splitConfiguration(
+                        mTestInfo, runConfig, mSuitePreparersPerDevice, 5, false, true);
         // matching 1 for 6 since tests sharding in 6 tests.
         assertEquals(6, res.size());
         // The original IRemoteTest does not exists anymore, new IRemoteTests have been created.
diff --git a/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java b/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
index 07e0807..81fa44f 100644
--- a/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
@@ -20,6 +20,7 @@
 
 import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
 import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.config.Configuration;
 import com.android.tradefed.config.ConfigurationDef;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.ConfigurationFactory;
@@ -87,6 +88,7 @@
     private IDeviceBuildInfo mBuildInfo;
     private ITestDevice mMockDevice;
     private TestInformation mTestInfo;
+    private IConfiguration mStubMainConfiguration;
 
     private static final String TEST_MAINLINE_CONFIG =
         "<configuration description=\"Runs a stub tests part of some suite\">\n"
@@ -115,6 +117,8 @@
         mMainlineRunner = new FakeMainlineTMSR();
         mMainlineRunner.setBuild(mBuildInfo);
         mMainlineRunner.setDevice(mMockDevice);
+        mStubMainConfiguration = new Configuration("stub", "stub");
+        mMainlineRunner.setConfiguration(mStubMainConfiguration);
         mMainlineOptionSetter = new OptionSetter(mMainlineRunner);
 
         IInvocationContext context = new InvocationContext();
diff --git a/tests/src/com/android/tradefed/testtype/suite/params/MainlineModuleHandlerTest.java b/tests/src/com/android/tradefed/testtype/suite/params/MainlineModuleHandlerTest.java
index 11dee18..4a5d707 100644
--- a/tests/src/com/android/tradefed/testtype/suite/params/MainlineModuleHandlerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/params/MainlineModuleHandlerTest.java
@@ -62,7 +62,7 @@
     public void testApplySetup() {
         EasyMock.expect(mMockBuildInfo.getBuildBranch()).andStubReturn("branch");
         EasyMock.replay(mMockBuildInfo);
-        mHandler = new MainlineModuleHandler("mod1.apk", mAbi, mContext);
+        mHandler = new MainlineModuleHandler("mod1.apk", mAbi, mContext, false);
         mHandler.applySetup(mConfig);
         assertTrue(mConfig.getTargetPreparers().get(0) instanceof InstallApexModuleTargetPreparer);
         InstallApexModuleTargetPreparer preparer =
@@ -77,7 +77,7 @@
     public void testApplySetup_MultipleMainlineModules() {
         EasyMock.expect(mMockBuildInfo.getBuildBranch()).andStubReturn("branch");
         EasyMock.replay(mMockBuildInfo);
-        mHandler = new MainlineModuleHandler("mod1.apk+mod2.apex", mAbi, mContext);
+        mHandler = new MainlineModuleHandler("mod1.apk+mod2.apex", mAbi, mContext, false);
         mHandler.applySetup(mConfig);
         assertTrue(mConfig.getTargetPreparers().get(0) instanceof InstallApexModuleTargetPreparer);
         InstallApexModuleTargetPreparer preparer =
@@ -97,7 +97,7 @@
         try {
             EasyMock.expect(mMockBuildInfo.getBuildBranch()).andStubReturn(null);
             EasyMock.replay(mMockBuildInfo);
-            mHandler = new MainlineModuleHandler("mod1.apk+mod2.apex", mAbi, mContext);
+            mHandler = new MainlineModuleHandler("mod1.apk+mod2.apex", mAbi, mContext, false);
             fail("Should have thrown an exception.");
         } catch (IllegalArgumentException expected) {
             // expected
diff --git a/tests/src/com/android/tradefed/util/AaptParserTest.java b/tests/src/com/android/tradefed/util/AaptParserTest.java
index 65bf70d..93caecf 100644
--- a/tests/src/com/android/tradefed/util/AaptParserTest.java
+++ b/tests/src/com/android/tradefed/util/AaptParserTest.java
@@ -168,6 +168,34 @@
         assertTrue(p.isRequestingLegacyStorage());
     }
 
+    public void testParseXmlTreeForAapt2_withRequestLegacyFlagTrue() {
+        AaptParser p = new AaptParser();
+        p.parseXmlTree(
+                "N: android=http://schemas.android.com/apk/res/android\n"
+                        + "  E: manifest (line=2)\n"
+                        + "    A: http://schemas.android.com/apk/res/android:versionCode(0x0101021b)=(type 0x10)0x1d\n"
+                        + "    A: http://schemas.android.com/apk/res/android:versionName(0x0101021c)=\"R\" (Raw: \"R\")\n"
+                        + "    A: http://schemas.android.com/apk/res/android:compileSdkVersion(0x01010572)=(type 0x10)0x1d\n"
+                        + "    A: http://schemas.android.com/apk/res/android:compileSdkVersionCodename(0x01010573)=\"R\" (Raw: "
+                        + "\"R\")\n"
+                        + "    A: package=\"com.android.foo\" (Raw: \"com.android.foo\")\n"
+                        + "    A: platformBuildVersionCode=(type 0x10)0x1d\n"
+                        + "    A: platformBuildVersionName=\"R\" (Raw: \"R\")\n"
+                        + "    E: uses-sdk (line=5)\n"
+                        + "      A: http://schemas.android.com/apk/res/android:minSdkVersion(0x0101020c)=(type 0x10)0x1c\n"
+                        + "      A: http://schemas.android.com/apk/res/android:targetSdkVersion(0x01010270)=\"R\" (Raw: \"R\")\n"
+                        + "    E: application (line=12)\n"
+                        + "      A: http://schemas.android.com/apk/res/android:targetSdkVersion(0x01010270)=(type 0x10)0x1e\n"
+                        + "      A: http://schemas.android.com/apk/res/android:supportsRtl(0x010103af)=(type 0x12)0xffffffff\n"
+                        + "      A: http://schemas.android.com/apk/res/android:extractNativeLibs(0x010104ea)=(type 0x12)0xffffffff\n"
+                        + "      A: http://schemas.android.com/apk/res/android:appComponentFactory(0x0101057a)=\"androidx.core.app"
+                        + ".CoreComponentFactory\" (Raw: \"androidx.core.app"
+                        + ".CoreComponentFactory\")\n"
+                        + "      A: http://schemas.android.com/apk/res/android:requestLegacyExternalStorage(0x01010603)=(type 0x12)"
+                        + "0xffffffff\n");
+        assertTrue(p.isRequestingLegacyStorage());
+    }
+
     public void testParseXmlTree_withRequestLegacyFlagFalse() {
         AaptParser p = new AaptParser();
         p.parseXmlTree(
@@ -196,6 +224,34 @@
         assertFalse(p.isRequestingLegacyStorage());
     }
 
+    public void testParseXmlTreeForAapt2_withRequestLegacyFlagFalse() {
+        AaptParser p = new AaptParser();
+        p.parseXmlTree(
+                "N: android=http://schemas.android.com/apk/res/android\n"
+                        + "  E: manifest (line=2)\n"
+                        + "    A: http://schemas.android.com/apk/res/android:versionCode(0x0101021b)=(type 0x10)0x1d\n"
+                        + "    A: http://schemas.android.com/apk/res/android:versionName(0x0101021c)=\"R\" (Raw: \"R\")\n"
+                        + "    A: http://schemas.android.com/apk/res/android:compileSdkVersion(0x01010572)=(type 0x10)0x1d\n"
+                        + "    A: http://schemas.android.com/apk/res/android:compileSdkVersionCodename(0x01010573)=\"R\" (Raw: "
+                        + "\"R\")\n"
+                        + "    A: package=\"com.android.foo\" (Raw: \"com.android.foo\")\n"
+                        + "    A: platformBuildVersionCode=(type 0x10)0x1d\n"
+                        + "    A: platformBuildVersionName=\"R\" (Raw: \"R\")\n"
+                        + "    E: uses-sdk (line=5)\n"
+                        + "      A: http://schemas.android.com/apk/res/android:minSdkVersion(0x0101020c)=(type 0x10)0x1c\n"
+                        + "      A: http://schemas.android.com/apk/res/android:targetSdkVersion(0x01010270)=\"R\" (Raw: \"R\")\n"
+                        + "    E: application (line=12)\n"
+                        + "      A: http://schemas.android.com/apk/res/android:targetSdkVersion(0x01010270)=(type 0x10)0x1e\n"
+                        + "      A: http://schemas.android.com/apk/res/android:supportsRtl(0x010103af)=(type 0x12)0xffffffff\n"
+                        + "      A: http://schemas.android.com/apk/res/android:extractNativeLibs(0x010104ea)=(type 0x12)0xffffffff\n"
+                        + "      A: http://schemas.android.com/apk/res/android:appComponentFactory(0x0101057a)=\"androidx.core.app"
+                        + ".CoreComponentFactory\" (Raw: \"androidx.core.app"
+                        + ".CoreComponentFactory\")\n"
+                        + "      A: http://schemas.android.com/apk/res/android:requestLegacyExternalStorage(0x01010603)=(type 0x12)"
+                        + "0x0\n");
+        assertFalse(p.isRequestingLegacyStorage());
+    }
+
     public void testParseXmlTree_withoutRequestLegacyFlag() {
         AaptParser p = new AaptParser();
         p.parseXmlTree(
@@ -221,7 +277,32 @@
         assertFalse(p.isRequestingLegacyStorage());
     }
 
-    public void testParseXmlTree_withUsesPermissionManageExternalStorage() {
+    public void testParseXmlTreeForAapt2_withoutRequestLegacyFlag() {
+        AaptParser p = new AaptParser();
+        p.parseXmlTree(
+                "N: android=http://schemas.android.com/apk/res/android\n"
+                        + "  E: manifest (line=2)\n"
+                        + "    A: http://schemas.android.com/apk/res/androidandroid:versionCode(0x0101021b)=(type 0x10)0x1d\n"
+                        + "    A: http://schemas.android.com/apk/res/androidandroid:versionName(0x0101021c)=\"R\" (Raw: \"R\")\n"
+                        + "    A: http://schemas.android.com/apk/res/androidandroid:compileSdkVersion(0x01010572)=(type 0x10)0x1d\n"
+                        + "    A: http://schemas.android.com/apk/res/androidandroid:compileSdkVersionCodename(0x01010573)=\"R\" (Raw: "
+                        + "\"R\")\n"
+                        + "    A: package=\"com.android.foo\" (Raw: \"com.android.foo\")\n"
+                        + "    A: platformBuildVersionCode=(type 0x10)0x1d\n"
+                        + "    A: platformBuildVersionName=\"R\" (Raw: \"R\")\n"
+                        + "    E: uses-sdk (line=5)\n"
+                        + "      A: http://schemas.android.com/apk/res/androidandroid:minSdkVersion(0x0101020c)=(type 0x10)0x1c\n"
+                        + "      A: http://schemas.android.com/apk/res/androidandroid:targetSdkVersion(0x01010270)=\"R\" (Raw: \"R\")\n"
+                        + "    E: application (line=12)\n"
+                        + "      A: http://schemas.android.com/apk/res/androidandroid:targetSdkVersion(0x01010270)=(type 0x10)0x1e\n"
+                        + "      A: http://schemas.android.com/apk/res/androidandroid:supportsRtl(0x010103af)=(type 0x12)0xffffffff\n"
+                        + "      A: http://schemas.android.com/apk/res/androidandroid:extractNativeLibs(0x010104ea)=(type 0x12)0xffffffff\n"
+                        + "      A: http://schemas.android.com/apk/res/androidandroid:appComponentFactory(0x0101057a)=\"androidx.core.app"
+                        + ".CoreComponentFactory\" (Raw: \"androidx.core.app");
+        assertFalse(p.isRequestingLegacyStorage());
+    }
+
+    public void testParse_withUsesPermissionManageExternalStorage() {
         AaptParser p = new AaptParser();
         p.parse(
                 "package: name='com.android.foo' versionCode='217173' versionName='1.7173' "
@@ -234,7 +315,7 @@
         assertTrue(p.isUsingPermissionManageExternalStorage());
     }
 
-    public void testParseXmlTree_withoutUsesPermissionManageExternalStorage() {
+    public void testParse_withoutUsesPermissionManageExternalStorage() {
         AaptParser p = new AaptParser();
         p.parse(
                 "package: name='com.android.foo' versionCode='217173' versionName='1.7173' "
diff --git a/tests/src/com/android/tradefed/util/AdbRootElevatorTest.java b/tests/src/com/android/tradefed/util/AdbRootElevatorTest.java
new file mode 100644
index 0000000..2e808d3
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/AdbRootElevatorTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.util;
+
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.INativeDevice;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link AdbRootElevator}. */
+@RunWith(JUnit4.class)
+public class AdbRootElevatorTest {
+    @Mock INativeDevice mMockDevice;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void testEnablesAndDisablesRoot() throws Exception {
+        when(mMockDevice.isAdbRoot()).thenReturn(false);
+        when(mMockDevice.enableAdbRoot()).thenReturn(true);
+
+        try (AdbRootElevator adbRoot = new AdbRootElevator(mMockDevice)) {
+            mMockDevice.waitForDeviceAvailable();
+        }
+
+        InOrder inOrder = Mockito.inOrder(mMockDevice);
+        inOrder.verify(mMockDevice).isAdbRoot();
+        inOrder.verify(mMockDevice).enableAdbRoot();
+        inOrder.verify(mMockDevice).waitForDeviceAvailable();
+        inOrder.verify(mMockDevice).disableAdbRoot();
+    }
+
+    @Test
+    public void testRootAlreadyEnabled_doesNotEnableOrDisableRoot() throws Exception {
+        when(mMockDevice.isAdbRoot()).thenReturn(true);
+
+        try (AdbRootElevator adbRoot = new AdbRootElevator(mMockDevice)) {
+            mMockDevice.waitForDeviceAvailable();
+        }
+
+        InOrder inOrder = Mockito.inOrder(mMockDevice);
+        inOrder.verify(mMockDevice).isAdbRoot();
+        inOrder.verify(mMockDevice).waitForDeviceAvailable();
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    public void testFailsToEnableAdbRoot_throwsException() throws Exception {
+        when(mMockDevice.isAdbRoot()).thenReturn(false);
+        when(mMockDevice.enableAdbRoot()).thenReturn(false);
+
+        try (AdbRootElevator adbRoot = new AdbRootElevator(mMockDevice)) {
+            fail("Exception should have already been thrown.");
+        } catch (Exception e) {
+            // Expected.
+        }
+
+        verify(mMockDevice, never()).disableAdbRoot();
+    }
+
+    @Test
+    public void testDeviceNotAvailableOnRoot_throwsException() throws Exception {
+        when(mMockDevice.isAdbRoot()).thenReturn(false);
+        when(mMockDevice.enableAdbRoot()).thenThrow(new DeviceNotAvailableException());
+
+        try (AdbRootElevator adbRoot = new AdbRootElevator(mMockDevice)) {
+            fail("Exception should have already been thrown.");
+        } catch (DeviceNotAvailableException e) {
+            // Expected.
+        }
+
+        verify(mMockDevice, never()).disableAdbRoot();
+    }
+}
diff --git a/tests/src/com/android/tradefed/util/ProtoUtilTest.java b/tests/src/com/android/tradefed/util/ProtoUtilTest.java
new file mode 100644
index 0000000..2e42eee
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/ProtoUtilTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.util;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import com.android.tradefed.util.test.ProtoUtilTestProto.TestMessage;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Unit tests for {@link ProtoUtil} */
+@RunWith(Parameterized.class)
+public class ProtoUtilTest {
+    @Parameter(0)
+    public String mTestName; // Unused, for identifying tests only.
+
+    @Parameter(1)
+    public TestMessage mMessage;
+
+    @Parameter(2)
+    public List<String> mReferences;
+
+    @Parameter(3)
+    public List<String> mExpectedResults;
+
+    @Parameters(name = "{0}#{index}")
+    public static Iterable<Object[]> data() {
+        List<Object[]> parameters = new ArrayList<>();
+        parameters.add(
+                new Object[] {
+                    "returnsMessageAsStringForEmptyReference",
+                    TestMessage.newBuilder().setIntField(7).build(),
+                    new ArrayList<String>(),
+                    Arrays.asList(TestMessage.newBuilder().setIntField(7).build().toString())
+                });
+        parameters.add(
+                new Object[] {
+                    "singleLevel",
+                    TestMessage.newBuilder().setIntField(7).build(),
+                    Arrays.asList("int_field"),
+                    Arrays.asList("7")
+                });
+        parameters.add(
+                new Object[] {
+                    "singleLevel",
+                    TestMessage.newBuilder().setStringField("string").build(),
+                    Arrays.asList("string_field"),
+                    Arrays.asList("string")
+                });
+        parameters.add(
+                new Object[] {
+                    "singleLevel",
+                    TestMessage.newBuilder()
+                            .setMessageField(TestMessage.SubMessage.newBuilder().setIntField(7))
+                            .build(),
+                    Arrays.asList("message_field"),
+                    Arrays.asList(TestMessage.SubMessage.newBuilder().setIntField(7).toString())
+                });
+        parameters.add(
+                new Object[] {
+                    "singleLevelRepeated",
+                    TestMessage.newBuilder()
+                            .addAllRepeatedStringField(Arrays.asList("string1", "string2"))
+                            .build(),
+                    Arrays.asList("repeated_string_field"),
+                    Arrays.asList("string1", "string2")
+                });
+        parameters.add(
+                new Object[] {
+                    "multiLevel",
+                    TestMessage.newBuilder()
+                            .setMessageField(TestMessage.SubMessage.newBuilder().setIntField(7))
+                            .build(),
+                    Arrays.asList("message_field", "int_field"),
+                    Arrays.asList("7")
+                });
+        parameters.add(
+                new Object[] {
+                    "multiLevelRepeated",
+                    TestMessage.newBuilder()
+                            .setMessageField(
+                                    TestMessage.SubMessage.newBuilder()
+                                            .addAllRepeatedStringField(
+                                                    Arrays.asList("string1", "string2")))
+                            .build(),
+                    Arrays.asList("message_field", "repeated_string_field"),
+                    Arrays.asList("string1", "string2")
+                });
+        parameters.add(
+                new Object[] {
+                    "multiLevelRepeated",
+                    TestMessage.newBuilder()
+                            .addAllRepeatedMessageField(
+                                    Arrays.asList(
+                                            TestMessage.SubMessage.newBuilder()
+                                                    .addAllRepeatedStringField(
+                                                            Arrays.asList("string1", "string2"))
+                                                    .build(),
+                                            TestMessage.SubMessage.newBuilder()
+                                                    .addAllRepeatedStringField(
+                                                            Arrays.asList("string3", "string4"))
+                                                    .build()))
+                            .build(),
+                    Arrays.asList("repeated_message_field", "repeated_string_field"),
+                    Arrays.asList("string1", "string2", "string3", "string4")
+                });
+        parameters.add(
+                new Object[] {
+                    "oneofSingleLevel",
+                    TestMessage.newBuilder().setOneofStringField("string").build(),
+                    Arrays.asList("oneof_string_field"),
+                    Arrays.asList("string")
+                });
+        parameters.add(
+                new Object[] {
+                    "oneofMultiLevel",
+                    TestMessage.newBuilder()
+                            .setOneofMessageField(
+                                    TestMessage.SubMessage.newBuilder()
+                                            .addAllRepeatedStringField(
+                                                    Arrays.asList("string1", "string2")))
+                            .build(),
+                    Arrays.asList("oneof_message_field", "repeated_string_field"),
+                    Arrays.asList("string1", "string2")
+                });
+        parameters.add(
+                new Object[] {
+                    "returnsEmptyForNonExistentFieldReference",
+                    TestMessage.newBuilder().setStringField("string").build(),
+                    Arrays.asList("not_a_field"),
+                    new ArrayList<String>()
+                });
+        parameters.add(
+                new Object[] {
+                    "returnsEmptyForNonExistentFieldReference",
+                    TestMessage.newBuilder()
+                            .setMessageField(TestMessage.SubMessage.newBuilder().setIntField(7))
+                            .build(),
+                    Arrays.asList("message_field", "not_a_field"),
+                    new ArrayList<String>()
+                });
+        parameters.add(
+                new Object[] {
+                    "returnsEmptyForNonExistentFieldReference",
+                    TestMessage.newBuilder()
+                            .setMessageField(TestMessage.SubMessage.newBuilder().setIntField(7))
+                            .build(),
+                    Arrays.asList("message_field", "int_field", "not_a_field"),
+                    new ArrayList<String>()
+                });
+        return parameters;
+    }
+
+    @Test
+    public void testParsing() {
+        assertArrayEquals(
+                mExpectedResults.toArray(),
+                ProtoUtil.getNestedFieldFromMessageAsStrings(mMessage, mReferences).toArray());
+    }
+}
diff --git a/tests/src/com/android/tradefed/util/PythonVirtualenvHelperTest.java b/tests/src/com/android/tradefed/util/PythonVirtualenvHelperTest.java
new file mode 100644
index 0000000..aeb1c24
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/PythonVirtualenvHelperTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.*;
+
+import com.google.common.base.Throwables;
+
+import org.junit.After;
+import org.junit.Test;
+
+import java.io.File;
+import java.nio.file.Paths;
+
+public class PythonVirtualenvHelperTest {
+
+    private File mVenvDir;
+
+    @After
+    public void tearDown() throws Exception {
+        FileUtil.recursiveDelete(mVenvDir);
+    }
+
+    @Test
+    public void testActivate_shouldThrowNPE_whenVirtualenvPathIsNull() throws Exception {
+        String nullVirtualenvPath = null;
+        IRunUtil runUtil = mock(RunUtil.class);
+
+        try {
+            PythonVirtualenvHelper.activate(runUtil, nullVirtualenvPath);
+            fail("Should have thrown an exception");
+        } catch (NullPointerException e) {
+            assertThat(
+                    String.format(
+                            "An unexpected exception was thrown, full stack trace: %s",
+                            Throwables.getStackTraceAsString(e)),
+                    e.getMessage(),
+                    containsString("Path to the Python virtual environment should not be null"));
+        }
+    }
+
+    @Test
+    public void testActivate_whenVirtualenvPathIsInvalid() throws Exception {
+        mVenvDir = FileUtil.createTempDir("venv");
+        mVenvDir.delete();
+        IRunUtil runUtil = mock(RunUtil.class);
+
+        try {
+            PythonVirtualenvHelper.activate(runUtil, mVenvDir.getAbsolutePath());
+            fail("Should have thrown an exception");
+        } catch (RuntimeException e) {
+            assertThat(
+                    String.format(
+                            "An unexpected exception was thrown, full stack trace: %s",
+                            Throwables.getStackTraceAsString(e)),
+                    e.getMessage(),
+                    containsString("Invalid python virtualenv path"));
+        }
+    }
+
+    @Test
+    public void testActivate_whenPythonBinNotFound() throws Exception {
+        mVenvDir = FileUtil.createTempDir("venv");
+        IRunUtil runUtil = mock(RunUtil.class);
+
+        try {
+            PythonVirtualenvHelper.activate(runUtil, mVenvDir.getAbsolutePath());
+            fail("Should have thrown an exception");
+        } catch (RuntimeException e) {
+            assertThat(
+                    String.format(
+                            "An unexpected exception was thrown, full stack trace: %s",
+                            Throwables.getStackTraceAsString(e)),
+                    e.getMessage(),
+                    containsString("Invalid python virtualenv path"));
+        }
+    }
+
+    @Test
+    public void testActivate_success() throws Exception {
+        mVenvDir = FileUtil.createTempDir("venv");
+        File pythonBin = new File(mVenvDir, "bin");
+        pythonBin.mkdir();
+        IRunUtil runUtil = mock(RunUtil.class);
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        result.setStdout(
+                "Name: pip\nLocation: "
+                        + Paths.get(mVenvDir.getAbsolutePath(), "lib/python3.8/site-packages"));
+        when(runUtil.runTimedCmd(anyLong(), anyString(), eq("show"), eq("pip"))).thenReturn(result);
+
+        PythonVirtualenvHelper.activate(runUtil, mVenvDir.getAbsolutePath());
+
+        verify(runUtil)
+                .setEnvVariable("PATH", pythonBin.getAbsolutePath() + ":" + System.getenv("PATH"));
+        verify(runUtil).setEnvVariable("VIRTUAL_ENV", mVenvDir.getAbsolutePath());
+        verify(runUtil)
+                .setEnvVariable(
+                        "PYTHONPATH",
+                        new File(mVenvDir, "lib/python3.8/site-packages").getAbsolutePath()
+                                + ":"
+                                + System.getenv("PYTHONPATH"));
+        verify(runUtil).unsetEnvVariable("PYTHONHOME");
+    }
+
+    @Test
+    public void testActivate_pipShowFails() throws Exception {
+        mVenvDir = FileUtil.createTempDir("venv");
+        File pythonBin = new File(mVenvDir, "bin");
+        pythonBin.mkdir();
+        IRunUtil runUtil = mock(RunUtil.class);
+        when(runUtil.runTimedCmd(anyLong(), anyString(), eq("show"), eq("pip")))
+                .thenReturn(new CommandResult());
+
+        try {
+            PythonVirtualenvHelper.activate(runUtil, mVenvDir.getAbsolutePath());
+            fail("Should have thrown an exception");
+        } catch (RuntimeException e) {
+            assertThat(
+                    String.format(
+                            "An unexpected exception was thrown, full stack trace: %s",
+                            Throwables.getStackTraceAsString(e)),
+                    e.getMessage(),
+                    containsString("pip3 show pip"));
+        }
+    }
+}
diff --git a/tests/src/com/android/tradefed/util/RemoteZipTest.java b/tests/src/com/android/tradefed/util/RemoteZipTest.java
index 0176a07..1d6056a 100644
--- a/tests/src/com/android/tradefed/util/RemoteZipTest.java
+++ b/tests/src/com/android/tradefed/util/RemoteZipTest.java
@@ -103,7 +103,7 @@
 
             List<CentralDirectoryInfo> entries = remoteZip.getZipEntries();
 
-            assertEquals(7, entries.size());
+            assertEquals(8, entries.size());
             assertTrue(mExpectedEntries.containsAll(entries));
         } finally {
             FileUtil.recursiveDelete(destDir);
@@ -122,7 +122,7 @@
             destDir = FileUtil.createTempDir("test");
             RemoteZip remoteZip = new RemoteZip(REMOTE_FILE, mZipFileSize, mDownloader, true);
             List<CentralDirectoryInfo> entries = remoteZip.getZipEntries();
-            assertEquals(7, entries.size());
+            assertEquals(8, entries.size());
             assertTrue(mExpectedEntries.containsAll(entries));
         } finally {
             FileUtil.recursiveDelete(destDir);
@@ -151,7 +151,7 @@
             targetFile = Paths.get(destDir.getPath(), "executable", "executable_file").toFile();
             assertTrue(targetFile.exists());
             // File not in the list is not unzipped.
-            targetFile = Paths.get(destDir.getPath(), "empty_file").toFile();
+            targetFile = Paths.get(destDir.getPath(), "empty/empty_file").toFile();
             assertFalse(targetFile.exists());
         } finally {
             FileUtil.recursiveDelete(destDir);
diff --git a/tests/src/com/android/tradefed/util/SparseImageUtilTest.java b/tests/src/com/android/tradefed/util/SparseImageUtilTest.java
new file mode 100644
index 0000000..d8858e7
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/SparseImageUtilTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+/** Unit tests for {@link SparseImageUtil} */
+@RunWith(JUnit4.class)
+public class SparseImageUtilTest {
+    private File mSparseImageFile;
+
+    @Before
+    public void setUp() throws IOException {
+        mSparseImageFile = FileUtil.createTempFile("sparse", ".img");
+        try (FileOutputStream out = new FileOutputStream(mSparseImageFile)) {
+            out.write(getSparseImageData());
+        }
+    }
+
+    @After
+    public void tearDown() {
+        FileUtil.deleteFile(mSparseImageFile);
+    }
+
+    /** Verify {@link com.android.tradefed.util.SparseImageUtil#isSparse}. */
+    @Test
+    public void testIsSparse() {
+        Assert.assertTrue(SparseImageUtil.isSparse(mSparseImageFile));
+    }
+
+    /** Verify {@link com.android.tradefed.util.SparseImageUtil#unsparse}. */
+    @Test
+    public void testUnsparse() throws IOException {
+        File unsparsedFile = FileUtil.createTempFile("unsparse", ".img");
+        byte[] unsparsedData = null;
+        try {
+            SparseImageUtil.unsparse(mSparseImageFile, unsparsedFile);
+            try (FileInputStream in = new FileInputStream(unsparsedFile)) {
+                unsparsedData = StreamUtil.getByteArrayListFromStream(in).getContents();
+            }
+            Assert.assertArrayEquals(getUnsparsedImageData(), unsparsedData);
+        } finally {
+            FileUtil.deleteFile(unsparsedFile);
+        }
+    }
+
+    /**
+     * Returns some sparse data.
+     *
+     * @see https://android.googlesource.com/platform/system/core/+/master/libsparse/sparse_format.h
+     */
+    private byte[] getSparseImageData() {
+        final int SPARSE_IMAGE_MAGIC = 0xED26FF3A;
+        ByteBuffer buffer = ByteBuffer.allocate(4096);
+        buffer.order(ByteOrder.LITTLE_ENDIAN);
+        // Header
+        buffer.putInt(SPARSE_IMAGE_MAGIC);
+        buffer.putShort((short) 1);
+        buffer.putShort((short) 0);
+        buffer.putShort((short) 28);
+        buffer.putShort((short) 12);
+        buffer.putInt(4); /* block size */
+        buffer.putInt(512 + 256); /* total blocks */
+        buffer.putInt(2); /* total chunks */
+        buffer.putInt(0); /* ignore check sum */
+        // RAW chunk, 2048 bytes of lorem ipsum
+        byte[] loremIpsum = getLoremIpsum();
+        buffer.putShort((short) 0xCAC1);
+        buffer.putShort((short) 0); /* padding */
+        buffer.putInt(loremIpsum.length / 4); /* data size in terms of number of blocks */
+        buffer.putInt(12 + loremIpsum.length); /* header size + data size */
+        buffer.put(loremIpsum);
+        // DONTCARE chunk, 1024 bytes of zeroes
+        byte[] zeroes = new byte[1024];
+        buffer.putShort((short) 0xCAC3);
+        buffer.putShort((short) 0); /* padding */
+        buffer.putInt(zeroes.length / 4); /* data size in terms of number of blocks */
+        buffer.putInt(12 + zeroes.length); /* header size + data size */
+        buffer.put(zeroes);
+        return Arrays.copyOf(buffer.array(), buffer.position());
+    }
+
+    private byte[] getUnsparsedImageData() {
+        byte[] loremIpsum = getLoremIpsum();
+        // Pad lorem ipsum with 1024 bytes of zeroes
+        return Arrays.copyOf(loremIpsum, loremIpsum.length + 1024);
+    }
+
+    /** Returns a chunk of text data. */
+    private byte[] getLoremIpsum() {
+        final int dataLen = 2048; /* Must be a multiple of 4 */
+        final String loremIpsumString =
+                "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor"
+                        + " incididunt ut labore et dolore magna aliqua. Enim neque volutpat ac"
+                        + " tincidunt vitae semper quis lectus. Est pellentesque elit ullamcorper"
+                        + " dignissim cras tincidunt lobortis feugiat vivamus. Vitae ultricies leo"
+                        + " integer malesuada nunc vel. Ultrices tincidunt arcu non sodales neque"
+                        + " sodales ut etiam sit. Arcu cursus vitae congue mauris rhoncus aenean."
+                        + " Consectetur a erat nam at lectus urna duis convallis convallis. Suscipit"
+                        + " tellus mauris a diam maecenas sed. At elementum eu facilisis sed odio."
+                        + " Neque sodales ut etiam sit.";
+        final byte[] loremIpsumBytes = loremIpsumString.getBytes();
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        while (buffer.size() < dataLen) {
+            buffer.write(loremIpsumBytes, 0, loremIpsumBytes.length);
+        }
+        return Arrays.copyOf(buffer.toByteArray(), dataLen);
+    }
+}
diff --git a/tests/src/com/android/tradefed/util/StringEscapeUtilsTest.java b/tests/src/com/android/tradefed/util/StringEscapeUtilsTest.java
index 7a81c5e..f166b6a 100644
--- a/tests/src/com/android/tradefed/util/StringEscapeUtilsTest.java
+++ b/tests/src/com/android/tradefed/util/StringEscapeUtilsTest.java
@@ -88,4 +88,30 @@
         assertArrayEquals(new String[]{"foo", "bar bar"},
                 StringEscapeUtils.paramsToArgs(expected).toArray());
     }
+
+    /**
+     * Simple test that {@link StringEscapeUtils#escapeShell(String)} escapes the is greater than
+     * sign.
+     */
+    @Test
+    public void testEscapesGreaterSigns() {
+        String escaped_str = StringEscapeUtils.escapeShell(">greater>signs");
+        assertEquals("\\>greater\\>signs", escaped_str);
+    }
+
+    /**
+     * Simple test that {@link StringEscapeUtils#escapeShell(String)} escapes the is less than sign.
+     */
+    @Test
+    public void testEscapesLessSigns() {
+        String escaped_str = StringEscapeUtils.escapeShell("<less<signs");
+        assertEquals("\\<less\\<signs", escaped_str);
+    }
+
+    /** Simple test that {@link StringEscapeUtils#escapeShell(String)} escapes the or sign. */
+    @Test
+    public void testEscapesOrSigns() {
+        String escaped_str = StringEscapeUtils.escapeShell("|or|signs");
+        assertEquals("\\|or\\|signs", escaped_str);
+    }
 }
diff --git a/tests/src/com/android/tradefed/util/ZipUtilTest.java b/tests/src/com/android/tradefed/util/ZipUtilTest.java
index 33841a0..cabf7fa 100644
--- a/tests/src/com/android/tradefed/util/ZipUtilTest.java
+++ b/tests/src/com/android/tradefed/util/ZipUtilTest.java
@@ -216,8 +216,8 @@
                             partialZipFile,
                             endCentralDirInfo,
                             endCentralDirInfo.getCentralDirOffset());
-            // The zip file has 3 folders, 4 files.
-            assertEquals(7, zipEntries.size());
+            // The zip file has 4 folders, 4 files.
+            assertEquals(8, zipEntries.size());
 
             CentralDirectoryInfo zipEntry;
             LocalFileHeader localFileHeader;
@@ -228,7 +228,7 @@
             zipEntry =
                     zipEntries
                             .stream()
-                            .filter(e -> e.getFileName().equals("empty_file"))
+                            .filter(e -> e.getFileName().equals("empty/empty_file"))
                             .findFirst()
                             .get();
             targetFile = new File(Paths.get(tmpDir.toString(), zipEntry.getFileName()).toString());
@@ -243,6 +243,7 @@
             // Verify file permissions - readonly - 644 rw-r--r--
             permissions = Files.getPosixFilePermissions(targetFile.toPath());
             assertEquals(PosixFilePermissions.fromString("rw-r--r--"), permissions);
+            assertTrue(targetFile.isFile());
 
             // Unzip text file
             zipEntry =
@@ -377,8 +378,8 @@
                             endCentralDirInfo,
                             endCentralDirInfo.getCentralDirOffset(),
                             true);
-            // The zip file has 3 folders, 4 files.
-            assertEquals(7, zipEntries.size());
+            // The zip file has 4 folders, 4 files.
+            assertEquals(8, zipEntries.size());
 
             CentralDirectoryInfo zipEntry;
             LocalFileHeader localFileHeader;
@@ -389,7 +390,7 @@
             zipEntry =
                     zipEntries
                             .stream()
-                            .filter(e -> e.getFileName().equals("empty_file"))
+                            .filter(e -> e.getFileName().equals("empty/empty_file"))
                             .findFirst()
                             .get();
             targetFile = new File(Paths.get(tmpDir.toString(), zipEntry.getFileName()).toString());
diff --git a/tests/src/com/android/tradefed/util/executor/ParallelDeviceExecutorTest.java b/tests/src/com/android/tradefed/util/executor/ParallelDeviceExecutorTest.java
index e27ed0e..b3ba079 100644
--- a/tests/src/com/android/tradefed/util/executor/ParallelDeviceExecutorTest.java
+++ b/tests/src/com/android/tradefed/util/executor/ParallelDeviceExecutorTest.java
@@ -31,6 +31,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.TimeUnit;
 
 /** Unit tests for {@link ParallelDeviceExecutor}. */
@@ -49,7 +50,7 @@
         mDevice2 = Mockito.mock(ITestDevice.class);
         mDevices.add(mDevice1);
         mDevices.add(mDevice2);
-        mExecutor = new ParallelDeviceExecutor<>(mDevices);
+        mExecutor = new ParallelDeviceExecutor<>(mDevices.size());
     }
 
     @Test
@@ -93,4 +94,23 @@
         assertTrue(mExecutor.getErrors().get(0).getMessage().contains("one"));
         assertTrue(mExecutor.getErrors().get(1).getMessage().contains("two"));
     }
+
+    @Test
+    public void testExecution_timeout() {
+        List<Callable<Boolean>> callableTasks = new ArrayList<>();
+        for (ITestDevice device : mDevices) {
+            callableTasks.add(
+                    () -> {
+                        Thread.sleep(1000L);
+                        return true;
+                    });
+        }
+
+        List<Boolean> results = mExecutor.invokeAll(callableTasks, 1L, TimeUnit.MILLISECONDS);
+        assertEquals(0, results.size());
+        assertTrue(mExecutor.hasErrors());
+        assertEquals(2, mExecutor.getErrors().size());
+        assertTrue(mExecutor.getErrors().get(0) instanceof CancellationException);
+        assertTrue(mExecutor.getErrors().get(1) instanceof CancellationException);
+    }
 }
diff --git a/tests/src/com/android/tradefed/util/statsd/MetricUtilTest.java b/tests/src/com/android/tradefed/util/statsd/MetricUtilTest.java
index dd43440..f45238a 100644
--- a/tests/src/com/android/tradefed/util/statsd/MetricUtilTest.java
+++ b/tests/src/com/android/tradefed/util/statsd/MetricUtilTest.java
@@ -176,12 +176,14 @@
                         any(CollectingByteOutputReceiver.class));
         List<EventMetricData> data = MetricUtil.getEventMetricData(mTestDevice, CONFIG_ID);
         // Resulting list should have two metrics.
-        assertThat(data.size()).comparesEqualTo(2);
+        assertThat(data.size()).isEquivalentAccordingToCompareTo(2);
         // The first metric should correspond to METRIC_2_* as its timestamp is earlier.
-        assertThat(data.get(0).getElapsedTimestampNanos()).comparesEqualTo(METRIC_2_NANOS);
+        assertThat(data.get(0).getElapsedTimestampNanos())
+                .isEquivalentAccordingToCompareTo(METRIC_2_NANOS);
         assertThat(data.get(0).getAtom().hasBleScanResultReceived()).isTrue();
         // The second metric should correspond to METRIC_1_*.
-        assertThat(data.get(1).getElapsedTimestampNanos()).comparesEqualTo(METRIC_1_NANOS);
+        assertThat(data.get(1).getElapsedTimestampNanos())
+                .isEquivalentAccordingToCompareTo(METRIC_1_NANOS);
         assertThat(data.get(1).getAtom().hasBleScanStateChanged()).isTrue();
     }
 
@@ -204,7 +206,7 @@
                         any(CollectingByteOutputReceiver.class));
         List<EventMetricData> data = MetricUtil.getEventMetricData(mTestDevice, CONFIG_ID);
         // Resulting list should be empty.
-        assertThat(data.size()).comparesEqualTo(0);
+        assertThat(data.size()).isEquivalentAccordingToCompareTo(0);
     }
 
     /**
@@ -230,12 +232,14 @@
                         any(CollectingByteOutputReceiver.class));
         List<EventMetricData> data = MetricUtil.getEventMetricData(mTestDevice, CONFIG_ID);
         // Resulting list should have two metrics.
-        assertThat(data.size()).comparesEqualTo(2);
+        assertThat(data.size()).isEquivalentAccordingToCompareTo(2);
         // The first metric should correspond to METRIC_2_* as its timestamp is earlier.
-        assertThat(data.get(0).getElapsedTimestampNanos()).comparesEqualTo(METRIC_2_NANOS);
+        assertThat(data.get(0).getElapsedTimestampNanos())
+                .isEquivalentAccordingToCompareTo(METRIC_2_NANOS);
         assertThat(data.get(0).getAtom().hasBleScanResultReceived()).isTrue();
         // The second metric should correspond to METRIC_1_*.
-        assertThat(data.get(1).getElapsedTimestampNanos()).comparesEqualTo(METRIC_1_NANOS);
+        assertThat(data.get(1).getElapsedTimestampNanos())
+                .isEquivalentAccordingToCompareTo(METRIC_1_NANOS);
         assertThat(data.get(1).getAtom().hasBleScanStateChanged()).isTrue();
     }
 
@@ -261,12 +265,14 @@
                         any(CollectingByteOutputReceiver.class));
         List<EventMetricData> data = MetricUtil.getEventMetricData(mTestDevice, CONFIG_ID);
         // Resulting list should have two metrics.
-        assertThat(data.size()).comparesEqualTo(2);
+        assertThat(data.size()).isEquivalentAccordingToCompareTo(2);
         // The first metric should correspond to METRIC_1_* as its timestamp is earlier.
-        assertThat(data.get(0).getElapsedTimestampNanos()).comparesEqualTo(METRIC_1_NANOS);
+        assertThat(data.get(0).getElapsedTimestampNanos())
+                .isEquivalentAccordingToCompareTo(METRIC_1_NANOS);
         assertThat(data.get(0).getAtom().hasBleScanStateChanged()).isTrue();
         // The second metric should correspond to METRIC_3_*.
-        assertThat(data.get(1).getElapsedTimestampNanos()).comparesEqualTo(METRIC_3_NANOS);
+        assertThat(data.get(1).getElapsedTimestampNanos())
+                .isEquivalentAccordingToCompareTo(METRIC_3_NANOS);
         assertThat(data.get(1).getAtom().hasBleScanStateChanged()).isTrue();
     }
 
diff --git a/util-apps/ContentProvider/main/AndroidManifest.xml b/util-apps/ContentProvider/main/AndroidManifest.xml
index ac37e68..15cb3fa 100644
--- a/util-apps/ContentProvider/main/AndroidManifest.xml
+++ b/util-apps/ContentProvider/main/AndroidManifest.xml
@@ -19,6 +19,7 @@
 
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
 
     <application>
         <provider
diff --git a/util-apps/DeviceSetupUtil/src/com/android/tradefed/utils/MainActivity.java b/util-apps/DeviceSetupUtil/src/com/android/tradefed/utils/MainActivity.java
index 2ecd90d..80a0352 100644
--- a/util-apps/DeviceSetupUtil/src/com/android/tradefed/utils/MainActivity.java
+++ b/util-apps/DeviceSetupUtil/src/com/android/tradefed/utils/MainActivity.java
@@ -20,7 +20,7 @@
 import android.os.Bundle;
 
 /**
- * Dummy activity used to launch app for first time, so in future it can handle Dismiss Keyguard
+ * An activity used to launch app for first time, so in future it can handle Dismiss Keyguard
  * intents on ICS and above devices.
  */
 public class MainActivity extends Activity {