Merge tag 'android-security-10.0.0_r53' into int/10/fp2

Android security 10.0.0 release 53

* tag 'android-security-10.0.0_r53':
  Merge "Ensure we take into account abi in exclude-filter" am: 16606dd95d am: be09346cba am: 909dd44e0f am: 56390dc187

Change-Id: I5a7867d69018c80c340000ddb47382b872af408d
diff --git a/atest/TEST_MAPPING b/atest/TEST_MAPPING
index 0769294..bcb8a43 100644
--- a/atest/TEST_MAPPING
+++ b/atest/TEST_MAPPING
@@ -1,4 +1,30 @@
 {
+  "mainline-presubmit": [
+    {
+      "name": "HelloWorldTests"
+    },
+    {
+      "name": "hello_world_test",
+      "host": true
+    },
+    {
+      "name": "CtsApacheHttpLegacy27ApiSignatureTestCases"
+    },
+    {
+      "name": "ziparchive-tests"
+    },
+    {
+      "name": "CtsDpiTestCases",
+      "options": [
+        {
+          "include-filter": "android.dpi.cts.ConfigurationScreenLayoutTest"
+        }
+      ]
+    },
+    {
+      "name": "CtsSampleHostTestCases"
+    }
+  ],
   "presubmit": [
     {
       "name": "atest_unittests",
diff --git a/atest/atest_utils.py b/atest/atest_utils.py
index 9e03e8e..8b2399f 100644
--- a/atest/atest_utils.py
+++ b/atest/atest_utils.py
@@ -174,9 +174,19 @@
     return False
 
 
-def get_result_server_args():
-    """Return list of args for communication with result server."""
+def get_result_server_args(for_test_mapping=False):
+    """Return list of args for communication with result server.
+
+    Args:
+        for_test_mapping: True if the test run is for Test Mapping to include
+            additional reporting args. Default is False.
+    """
+    # TODO (b/147644460) Temporarily disable Sponge V1 since it will be turned
+    # down.
     if _can_upload_to_result_server():
+        if for_test_mapping:
+            return (constants.RESULT_SERVER_ARGS +
+                    constants.TEST_MAPPING_RESULT_SERVER_ARGS)
         return constants.RESULT_SERVER_ARGS
     return []
 
diff --git a/atest/test_runners/atest_tf_test_runner.py b/atest/test_runners/atest_tf_test_runner.py
index a4f62d5..c895b36 100644
--- a/atest/test_runners/atest_tf_test_runner.py
+++ b/atest/test_runners/atest_tf_test_runner.py
@@ -388,7 +388,10 @@
             logging.info('%s does not support the following args %s',
                          self.EXECUTABLE, args_not_supported)
 
-        test_args.extend(atest_utils.get_result_server_args())
+        # Only need to check one TestInfo to determine if the tests are
+        # configured in TEST_MAPPING.
+        for_test_mapping = test_infos and test_infos[0].from_test_mapping
+        test_args.extend(atest_utils.get_result_server_args(for_test_mapping))
         self.run_cmd_dict['args'] = ' '.join(test_args)
         return [self._RUN_CMD.format(**self.run_cmd_dict)]
 
@@ -495,10 +498,6 @@
         if not test_infos:
             return []
 
-        # Only need to check one TestInfo to determine if the tests are
-        # configured in TEST_MAPPING.
-        if test_infos[0].from_test_mapping:
-            args.extend(constants.TEST_MAPPING_RESULT_SERVER_ARGS)
         test_infos = self._flatten_test_infos(test_infos)
 
         for info in test_infos:
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
new file mode 100644
index 0000000..c551496
--- /dev/null
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/suite/CertificationSuiteResultReporter.java
@@ -0,0 +1,597 @@
+/*
+ * 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
new file mode 100644
index 0000000..295692b
--- /dev/null
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/suite/PreviousSessionFileCopier.java
@@ -0,0 +1,138 @@
+/*
+ * 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
new file mode 100644
index 0000000..805a2ad
--- /dev/null
+++ b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/ResultReporterTest.java
@@ -0,0 +1,622 @@
+/*
+ * 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
new file mode 100644
index 0000000..a0c3131
--- /dev/null
+++ b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/suite/PreviousSessionFileCopierTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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/src/com/android/tradefed/config/Configuration.java b/src/com/android/tradefed/config/Configuration.java
index 9982d59..280b0a7 100644
--- a/src/com/android/tradefed/config/Configuration.java
+++ b/src/com/android/tradefed/config/Configuration.java
@@ -41,6 +41,7 @@
 import com.android.tradefed.targetprep.multi.IMultiTargetPreparer;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.StubTest;
+import com.android.tradefed.testtype.coverage.CoverageOptions;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.MultiMap;
 import com.android.tradefed.util.QuotationAwareTokenizer;
@@ -98,6 +99,7 @@
     public static final String METRIC_POST_PROCESSOR_TYPE_NAME = "metric_post_processor";
     public static final String SANDBOX_TYPE_NAME = "sandbox";
     public static final String SANBOX_OPTIONS_TYPE_NAME = "sandbox_options";
+    public static final String COVERAGE_OPTIONS_TYPE_NAME = "coverage";
 
     private static Map<String, ObjTypeInfo> sObjTypeMap = null;
     private static Set<String> sMultiDeviceSupportedTag = null;
@@ -185,6 +187,8 @@
                     METRIC_POST_PROCESSOR_TYPE_NAME,
                     new ObjTypeInfo(BasePostProcessor.class, true));
             sObjTypeMap.put(SANBOX_OPTIONS_TYPE_NAME, new ObjTypeInfo(SandboxOptions.class, false));
+            sObjTypeMap.put(
+                    COVERAGE_OPTIONS_TYPE_NAME, new ObjTypeInfo(CoverageOptions.class, false));
         }
         return sObjTypeMap;
     }
@@ -239,6 +243,7 @@
         setConfigurationDescriptor(new ConfigurationDescriptor());
         setDeviceMetricCollectors(new ArrayList<>());
         setPostProcessors(new ArrayList<>());
+        setCoverageOptions(new CoverageOptions());
         setConfigurationObjectNoThrow(SANBOX_OPTIONS_TYPE_NAME, new SandboxOptions());
     }
 
@@ -472,6 +477,13 @@
         return (List<IDeviceConfiguration>)getConfigurationObjectList(DEVICE_NAME);
     }
 
+    /** {@inheritDoc} */
+    @SuppressWarnings("unchecked")
+    @Override
+    public CoverageOptions getCoverageOptions() {
+        return (CoverageOptions) getConfigurationObject(COVERAGE_OPTIONS_TYPE_NAME);
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -763,6 +775,12 @@
         setConfigurationObjectListNoThrow(DEVICE_NAME, deviceConfigs);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void setCoverageOptions(CoverageOptions coverageOptions) {
+        setConfigurationObjectNoThrow(COVERAGE_OPTIONS_TYPE_NAME, coverageOptions);
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -1490,6 +1508,12 @@
                 getConfigurationObject(SANBOX_OPTIONS_TYPE_NAME),
                 excludeFilters,
                 printDeprecatedOptions);
+        ConfigurationUtil.dumpClassToXml(
+                serializer,
+                COVERAGE_OPTIONS_TYPE_NAME,
+                getCoverageOptions(),
+                excludeFilters,
+                printDeprecatedOptions);
 
         serializer.endTag(null, ConfigurationUtil.CONFIGURATION_NAME);
         serializer.endDocument();
diff --git a/src/com/android/tradefed/config/IConfiguration.java b/src/com/android/tradefed/config/IConfiguration.java
index bc8d1dd..604b7f6 100644
--- a/src/com/android/tradefed/config/IConfiguration.java
+++ b/src/com/android/tradefed/config/IConfiguration.java
@@ -32,6 +32,7 @@
 import com.android.tradefed.targetprep.ITargetPreparer;
 import com.android.tradefed.targetprep.multi.IMultiTargetPreparer;
 import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.coverage.CoverageOptions;
 import com.android.tradefed.util.keystore.IKeyStoreClient;
 
 import org.json.JSONArray;
@@ -163,6 +164,13 @@
     public IDeviceSelection getDeviceRequirements();
 
     /**
+     * Gets the {@link CoverageOptions} to use from the configuration.
+     *
+     * @return the {@link CoverageOptions} provided in the configuration.
+     */
+    public CoverageOptions getCoverageOptions();
+
+    /**
      * Generic interface to get the configuration object with the given type name.
      *
      * @param typeName the unique type of the configuration object
@@ -421,6 +429,9 @@
      */
     public void setDeviceOptions(TestDeviceOptions deviceOptions);
 
+    /** Set the {@link CoverageOptions}, replacing any existing values. */
+    public void setCoverageOptions(CoverageOptions coverageOptions);
+
     /**
      * Generic method to set the config object with the given name, replacing any existing value.
      *
diff --git a/src/com/android/tradefed/device/INativeDevice.java b/src/com/android/tradefed/device/INativeDevice.java
index 99a1b04..50be15e 100644
--- a/src/com/android/tradefed/device/INativeDevice.java
+++ b/src/com/android/tradefed/device/INativeDevice.java
@@ -1184,6 +1184,13 @@
     public void remountSystemWritable() throws DeviceNotAvailableException;
 
     /**
+     * Make the vendor partition on the device writable. May reboot the device.
+     *
+     * @throws DeviceNotAvailableException
+     */
+    public void remountVendorWritable() throws DeviceNotAvailableException;
+
+    /**
      * Returns the key type used to sign the device image
      * <p>
      * Typically Android devices may be signed with test-keys (like in AOSP) or release-keys
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index 377abc1..c129357 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -3915,6 +3915,20 @@
         waitForDeviceAvailable();
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void remountVendorWritable() throws DeviceNotAvailableException {
+        String verity = getProperty("partition.vendor.verified");
+        // have the property set (regardless state) implies verity is enabled, so we send adb
+        // command to disable verity
+        if (verity != null && !verity.isEmpty()) {
+            executeAdbCommand("disable-verity");
+            reboot();
+        }
+        executeAdbCommand("remount");
+        waitForDeviceAvailable();
+    }
+
     /**
      * {@inheritDoc}
      */
diff --git a/src/com/android/tradefed/device/TestDevice.java b/src/com/android/tradefed/device/TestDevice.java
index c82a8bd..eb06d2e 100644
--- a/src/com/android/tradefed/device/TestDevice.java
+++ b/src/com/android/tradefed/device/TestDevice.java
@@ -1401,9 +1401,12 @@
      */
     @Override
     public boolean hasFeature(String feature) throws DeviceNotAvailableException {
-        final String output = executeShellCommand("pm list features");
-        if (output.contains(feature)) {
-            return true;
+        String commandOutput = executeShellCommand("pm list features");
+        for (String line: commandOutput.split("\\s+")) {
+            // Each line in the output of the command has the format "feature:{FEATURE_VALUE}".
+            if (feature.equals(line)) {
+                return true;
+            }
         }
         CLog.w("Feature: %s is not available on %s", feature, getSerialNumber());
         return false;
diff --git a/src/com/android/tradefed/device/metric/BaseDeviceMetricCollector.java b/src/com/android/tradefed/device/metric/BaseDeviceMetricCollector.java
index 1e4d16c..10885cf 100644
--- a/src/com/android/tradefed/device/metric/BaseDeviceMetricCollector.java
+++ b/src/com/android/tradefed/device/metric/BaseDeviceMetricCollector.java
@@ -130,6 +130,15 @@
         // Does nothing
     }
 
+    @Override
+    public void onTestEnd(
+            DeviceMetricData testData,
+            final Map<String, Metric> currentTestCaseMetrics,
+            TestDescription test) {
+        // Call the default implementation of onTestEnd if not overridden
+        onTestEnd(testData, currentTestCaseMetrics);
+    }
+
     /** =================================== */
     /** Invocation Listeners for forwarding */
     @Override
@@ -233,7 +242,7 @@
             TestDescription test, long endTime, HashMap<String, Metric> testMetrics) {
         if (!mSkipTestCase) {
             try {
-                onTestEnd(mTestData, testMetrics);
+                onTestEnd(mTestData, testMetrics, test);
                 mTestData.addToMetrics(testMetrics);
             } catch (Throwable t) {
                 // Prevent exception from messing up the status reporting.
diff --git a/src/com/android/tradefed/device/metric/IMetricCollector.java b/src/com/android/tradefed/device/metric/IMetricCollector.java
index 387e536..1c8b84f 100644
--- a/src/com/android/tradefed/device/metric/IMetricCollector.java
+++ b/src/com/android/tradefed/device/metric/IMetricCollector.java
@@ -113,4 +113,18 @@
      */
     public void onTestEnd(
             DeviceMetricData testData, final Map<String, Metric> currentTestCaseMetrics);
+
+    /**
+     * Callback when a test case is ended. This should be the time for clean up.
+     *
+     * @param testData the {@link DeviceMetricData} holding the data for the test case. Will be the
+     *     same object as during {@link #onTestStart(DeviceMetricData)}.
+     * @param currentTestCaseMetrics the current map of metrics passed to {@link
+     *     #testEnded(TestDescription, Map)}.
+     * @param test the {@link TestDescription} of the test case in progress.
+     */
+    public void onTestEnd(
+            DeviceMetricData testData,
+            final Map<String, Metric> currentTestCaseMetrics,
+            TestDescription test);
 }
diff --git a/src/com/android/tradefed/host/HostOptions.java b/src/com/android/tradefed/host/HostOptions.java
index 8820eff..cb2f946 100644
--- a/src/com/android/tradefed/host/HostOptions.java
+++ b/src/com/android/tradefed/host/HostOptions.java
@@ -65,6 +65,11 @@
     )
     private Map<String, File> mJsonServiceAccountMap = new HashMap<>();
 
+    @Option(
+            name = "use-zip64-in-partial-download",
+            description = "Whether to use zip64 format in partial download.")
+    private boolean mUseZip64InPartialDownload = false;
+
     /**
      * {@inheritDoc}
      */
@@ -108,4 +113,10 @@
     public void validateOptions() throws ConfigurationException {
         // Validation of host options
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean getUseZip64InPartialDownload() {
+        return mUseZip64InPartialDownload;
+    }
 }
diff --git a/src/com/android/tradefed/host/IHostOptions.java b/src/com/android/tradefed/host/IHostOptions.java
index eb36dda..9c15151 100644
--- a/src/com/android/tradefed/host/IHostOptions.java
+++ b/src/com/android/tradefed/host/IHostOptions.java
@@ -56,4 +56,7 @@
 
     /** Validate that the options set on {@link IHostOptions} are valid. */
     void validateOptions() throws ConfigurationException;
+
+    /** Check if it should use the zip64 format in partial download or not. */
+    boolean getUseZip64InPartialDownload();
 }
diff --git a/src/com/android/tradefed/result/ATestFileSystemLogSaver.java b/src/com/android/tradefed/result/ATestFileSystemLogSaver.java
new file mode 100644
index 0000000..eec9a0e
--- /dev/null
+++ b/src/com/android/tradefed/result/ATestFileSystemLogSaver.java
@@ -0,0 +1,42 @@
+/*
+ * 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.result;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+
+/** This LogSaver class is used by ATest to save logs in a specific path. */
+@OptionClass(alias = "atest-file-system-log-saver")
+public class ATestFileSystemLogSaver extends FileSystemLogSaver {
+
+    @Option(name = "atest-log-file-path", description = "root file system path to store log files.")
+    private File mAtestRootReportDir = new File(System.getProperty("java.io.tmpdir"));
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The directory created in the path that option atest-log-file-path specify.
+     */
+    @Override
+    protected File generateLogReportDir(IBuildInfo buildInfo, File reportDir) throws IOException {
+        return FileUtil.createTempDir("invocation_", mAtestRootReportDir);
+    }
+}
diff --git a/src/com/android/tradefed/result/FileSystemLogSaver.java b/src/com/android/tradefed/result/FileSystemLogSaver.java
index 35b83e6..f8ba5f0 100644
--- a/src/com/android/tradefed/result/FileSystemLogSaver.java
+++ b/src/com/android/tradefed/result/FileSystemLogSaver.java
@@ -195,8 +195,7 @@
         File logReportDir;
         // now create unique directory within the buildDir
         try {
-            File buildDir = createBuildDir(buildInfo, reportDir);
-            logReportDir = FileUtil.createTempDir("inv_", buildDir);
+            logReportDir = generateLogReportDir(buildInfo, reportDir);
         } catch (IOException e) {
             CLog.e("Unable to create unique directory in %s. Attempting to use tmp dir instead",
                     reportDir.getAbsolutePath());
@@ -218,6 +217,18 @@
     }
 
     /**
+     * An exposed method that allow subclass to customize generating path logic.
+     *
+     * @param buildInfo the {@link IBuildInfo}
+     * @param reportDir the {@link File} for the report directory.
+     * @return The directory created.
+     */
+    protected File generateLogReportDir(IBuildInfo buildInfo, File reportDir) throws IOException {
+        File buildDir = createBuildDir(buildInfo, reportDir);
+        return FileUtil.createTempDir("inv_", buildDir);
+    }
+
+    /**
      * A helper method to get or create a build directory based on the build info of the invocation.
      * <p>
      * Create a unique file system directory with the structure
diff --git a/src/com/android/tradefed/result/LogDataType.java b/src/com/android/tradefed/result/LogDataType.java
index 9589c90..2024bf1 100644
--- a/src/com/android/tradefed/result/LogDataType.java
+++ b/src/com/android/tradefed/result/LogDataType.java
@@ -27,6 +27,8 @@
     MP4("mp4", "video/mp4", true, false),
     EAR("ear", "application/octet-stream", true, false),
     ZIP("zip", "application/zip", true, false),
+    SEVEN_Z("7z", "application/x-7z-compressed", true, false),
+    BITS("bits", "application/octet-stream", true, false),
     JPEG("jpeg", "image/jpeg", true, false),
     TAR_GZ("tar.gz", "application/gzip", true, false),
     GZIP("gz", "application/gzip", true, false),
diff --git a/src/com/android/tradefed/targetprep/RebootTargetPreparer.java b/src/com/android/tradefed/targetprep/RebootTargetPreparer.java
index 5b9a87d..ff8b42b 100644
--- a/src/com/android/tradefed/targetprep/RebootTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/RebootTargetPreparer.java
@@ -13,20 +13,38 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package com.android.tradefed.targetprep;
 
 import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 
 /** Target preparer that reboots the device. */
 @OptionClass(alias = "reboot-preparer")
-public class RebootTargetPreparer extends BaseTargetPreparer {
+public class RebootTargetPreparer extends BaseTargetPreparer implements ITargetCleaner {
+
+    @Option(name = "pre-reboot", description = "Reboot the device during setUp.")
+    private boolean mPreReboot = true;
+
+    @Option(name = "post-reboot", description = "Reboot the device during tearDown.")
+    private boolean mPostReboot = false;
 
     @Override
     public void setUp(ITestDevice device, IBuildInfo buildInfo)
             throws TargetSetupError, BuildError, DeviceNotAvailableException {
-        device.reboot();
+        if (mPreReboot) {
+            device.reboot();
+        }
+    }
+
+    @Override
+    public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+            throws DeviceNotAvailableException {
+        if (mPostReboot) {
+            device.reboot();
+        }
     }
 }
diff --git a/src/com/android/tradefed/targetprep/TemperatureThrottlingWaiter.java b/src/com/android/tradefed/targetprep/TemperatureThrottlingWaiter.java
index f776296..598d78a 100644
--- a/src/com/android/tradefed/targetprep/TemperatureThrottlingWaiter.java
+++ b/src/com/android/tradefed/targetprep/TemperatureThrottlingWaiter.java
@@ -23,6 +23,9 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.RunUtil;
 
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
 /** An {@link ITargetPreparer} that waits until device's temperature gets down to target */
 @OptionClass(alias = "temperature-throttle-waiter")
 public class TemperatureThrottlingWaiter extends BaseTargetPreparer {
@@ -46,11 +49,10 @@
     public static final String DEVICE_TEMPERATURE_FILE_PATH_NAME = "device-temperature-file-path";
 
     @Option(
-        name = DEVICE_TEMPERATURE_FILE_PATH_NAME,
-        description =
-                "Name of file that contains device"
-                        + "temperature. Example: /sys/class/hwmon/hwmon1/device/msm_therm"
-    )
+            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 = "target-temperature", description = "Target Temperature that device should have;"
@@ -110,23 +112,30 @@
         String result = device.executeShellCommand(
                 String.format("cat %s", fileName)).trim();
         CLog.i(String.format("Temperature file output : %s", result));
-        // example output : Result:30 Raw:7f6f
+
         if (result == null || result.contains("No such file or directory")) {
             throw new TargetSetupError(String.format("File %s doesn't exist", fileName),
                     device.getDeviceDescriptor());
-        } else if (!result.toLowerCase().startsWith("result:")) {
-            throw new TargetSetupError(
-                    String.format("file content is not as expected. Content : %s", result),
-                    device.getDeviceDescriptor());
         }
 
-        try {
-            deviceTemp = Integer.parseInt(result.split(" ")[0].split(":")[1].trim());
-        } catch (NumberFormatException numEx) {
-            CLog.e(String.format("Temperature is not of right format %s", numEx.getMessage()));
-            throw numEx;
+        // temperature raw format example output : Result:30 Raw:7f6f
+        final Pattern TEMPERATURE_RAW_FORMAT_REGEX = Pattern.compile("Result:(\\d+)\\sRaw:(\\w+)");
+        // temperature format example output : 30
+        final Pattern TEMPERATURE_FORMAT_REGEX = Pattern.compile("\\d+");
+        Matcher matcher = TEMPERATURE_RAW_FORMAT_REGEX.matcher(result);
+        if (matcher.find()) {
+            deviceTemp = Integer.parseInt(matcher.group(1));
+        } else {
+            matcher = TEMPERATURE_FORMAT_REGEX.matcher(result);
+            if (matcher.find()) {
+                deviceTemp = Integer.parseInt(matcher.group());
+            } else {
+                throw new TargetSetupError(
+                        String.format("file content is not as expected. Content : %s", result),
+                        device.getDeviceDescriptor());
+            }
         }
 
         return deviceTemp;
     }
-}
\ No newline at end of file
+}
diff --git a/src/com/android/tradefed/testtype/GTest.java b/src/com/android/tradefed/testtype/GTest.java
index d81ec95..9da6620 100644
--- a/src/com/android/tradefed/testtype/GTest.java
+++ b/src/com/android/tradefed/testtype/GTest.java
@@ -16,6 +16,8 @@
 
 package com.android.tradefed.testtype;
 
+import static com.android.tradefed.testtype.coverage.CoverageOptions.Toolchain.GCOV;
+
 import com.android.ddmlib.FileListingService;
 import com.android.ddmlib.IShellOutputReceiver;
 import com.android.tradefed.config.Option;
@@ -25,7 +27,9 @@
 import com.android.tradefed.device.ITestDevice;
 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.FileUtil;
+import com.android.tradefed.util.NativeCodeCoverageFlusher;
 
 import com.google.common.annotations.VisibleForTesting;
 
@@ -34,6 +38,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
@@ -58,6 +63,30 @@
             description = "Stops the Java application runtime before test execution.")
     private boolean mStopRuntime = false;
 
+    /** @deprecated Use the --coverage-flush option in CoverageOptions instead. */
+    @Deprecated
+    @Option(
+        name = "coverage-flush",
+        description = "Forces coverage data to be flushed at the end of the test."
+    )
+    private boolean mCoverageFlush = false;
+
+    /** @deprecated Use the --coverage-processes option in CoverageOptions instead. */
+    @Deprecated
+    @Option(
+        name = "coverage-processes",
+        description = "Name of processes to collect coverage data from."
+    )
+    private List<String> mCoverageProcesses = new ArrayList<>();
+
+    /** @deprecated Merged into the --coverage-flush option in CoverageOptions instead. */
+    @Deprecated
+    @Option(
+        name = "coverage-clear-before-test",
+        description = "Clears all coverage counters before test execution."
+    )
+    private boolean mCoverageClearBeforeTest = true;
+
     // Max characters allowed for executing GTest via command line
     private static final int GTEST_CMD_CHAR_LIMIT = 1000;
     /**
@@ -344,9 +373,20 @@
             mDevice.executeShellCommand("stop");
         }
         // Insert the coverage listener if code coverage collection is enabled.
-        listener = addNativeCoverageListenerIfEnabled(mDevice, listener);
+        listener = addNativeCoverageListenerIfEnabled(listener);
+        NativeCodeCoverageFlusher flusher =
+                new NativeCodeCoverageFlusher(mDevice, getCoverageOptions().getCoverageProcesses());
+
         Throwable throwable = null;
         try {
+            if (getCoverageOptions().isCoverageEnabled()) {
+                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;
@@ -360,4 +400,21 @@
             }
         }
     }
+
+    /**
+     * 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/src/com/android/tradefed/testtype/GTestBase.java b/src/com/android/tradefed/testtype/GTestBase.java
index a03e907..f003160 100644
--- a/src/com/android/tradefed/testtype/GTestBase.java
+++ b/src/com/android/tradefed/testtype/GTestBase.java
@@ -17,12 +17,14 @@
 package com.android.tradefed.testtype;
 
 import com.android.ddmlib.IShellOutputReceiver;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.IConfigurationReceiver;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionCopier;
 import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
 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.google.common.annotations.VisibleForTesting;
@@ -36,6 +38,7 @@
 /** The base class of gTest */
 public abstract class GTestBase
         implements IRemoteTest,
+                IConfigurationReceiver,
                 ITestFilterReceiver,
                 IRuntimeHintProvider,
                 ITestCollector,
@@ -89,6 +92,8 @@
             isTimeVal = true)
     private long mMaxTestTimeMs = 1 * 60 * 1000L;
 
+    /** @deprecated use --coverage in CoverageOptions instead. */
+    @Deprecated
     @Option(
         name = "coverage",
         description =
@@ -166,6 +171,14 @@
     private int mShardIndex = 0;
     private boolean mIsSharded = false;
 
+    private IConfiguration mConfiguration = null;
+
+    /** {@inheritDoc} */
+    @Override
+    public void setConfiguration(IConfiguration configuration) {
+        mConfiguration = configuration;
+    }
+
     /**
      * Set the Android native test module to run.
      *
@@ -484,6 +497,10 @@
             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));
@@ -517,21 +534,6 @@
     }
 
     /**
-     * Adds a {@link NativeCodeCoverageListener} to the chain if code coverage is enabled.
-     *
-     * @param device the device to pull the coverage results from
-     * @param listener the original listener
-     * @return a chained listener if code coverage is enabled, otherwise the original listener
-     */
-    protected ITestInvocationListener addNativeCoverageListenerIfEnabled(
-            ITestDevice device, ITestInvocationListener listener) {
-        if (mCoverage) {
-            return new NativeCodeCoverageListener(device, listener);
-        }
-        return listener;
-    }
-
-    /**
      * Make a best effort attempt to retrieve a meaningful short descriptive message for given
      * {@link Exception}
      *
@@ -590,4 +592,15 @@
         }
         return shard;
     }
+
+    /**
+     * 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();
+    }
 }
diff --git a/src/com/android/tradefed/testtype/InstrumentationFileTest.java b/src/com/android/tradefed/testtype/InstrumentationFileTest.java
index 0361393..be791e8 100644
--- a/src/com/android/tradefed/testtype/InstrumentationFileTest.java
+++ b/src/com/android/tradefed/testtype/InstrumentationFileTest.java
@@ -81,6 +81,7 @@
         mInstrumentationTest = createInstrumentationTest();
         // copy all options from the original InstrumentationTest
         OptionCopier.copyOptions(instrumentationTest, mInstrumentationTest);
+        mInstrumentationTest.setConfiguration(instrumentationTest.getConfiguration());
         mInstrumentationTest.setDevice(instrumentationTest.getDevice());
         mInstrumentationTest.setForceAbi(instrumentationTest.getForceAbi());
         mInstrumentationTest.setReRunUsingTestFile(true);
diff --git a/src/com/android/tradefed/testtype/InstrumentationSerialTest.java b/src/com/android/tradefed/testtype/InstrumentationSerialTest.java
index ef2de3c..bf2eb98 100644
--- a/src/com/android/tradefed/testtype/InstrumentationSerialTest.java
+++ b/src/com/android/tradefed/testtype/InstrumentationSerialTest.java
@@ -71,6 +71,7 @@
             throws ConfigurationException {
         InstrumentationTest runner = new InstrumentationTest();
         OptionCopier.copyOptions(instrumentationTest, runner);
+        runner.setConfiguration(instrumentationTest.getConfiguration());
         runner.setDevice(instrumentationTest.getDevice());
         runner.setForceAbi(instrumentationTest.getForceAbi());
         // ensure testFile is not used.
diff --git a/src/com/android/tradefed/testtype/InstrumentationTest.java b/src/com/android/tradefed/testtype/InstrumentationTest.java
index 372bd5f..7c1c471 100644
--- a/src/com/android/tradefed/testtype/InstrumentationTest.java
+++ b/src/com/android/tradefed/testtype/InstrumentationTest.java
@@ -16,6 +16,9 @@
 
 package com.android.tradefed.testtype;
 
+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;
 
@@ -26,6 +29,8 @@
 import com.android.ddmlib.testrunner.InstrumentationResultParser;
 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
 import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.IConfigurationReceiver;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.Option.Importance;
 import com.android.tradefed.config.OptionClass;
@@ -43,10 +48,14 @@
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.result.ddmlib.DefaultRemoteAndroidTestRunner;
+import com.android.tradefed.testtype.coverage.CoverageOptions;
+import com.android.tradefed.testtype.coverage.CoverageOptions.Toolchain;
 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;
@@ -70,6 +79,7 @@
                 IResumableTest,
                 ITestCollector,
                 IAbiReceiver,
+                IConfigurationReceiver,
                 IInvocationContextReceiver,
                 IMetricCollectorReceiver {
 
@@ -244,6 +254,8 @@
     )
     protected boolean mDebug = false;
 
+    /** @deprecated Use the --coverage option in CoverageOptions instead. */
+    @Deprecated
     @Option(
         name = "coverage",
         description =
@@ -297,14 +309,27 @@
     private String mTestFilePathOnDevice = null;
 
     private ListInstrumentationParser mListInstrumentationParser = null;
+    private NativeCodeCoverageListener mNativeCoverageListener = null;
 
     private Set<String> mExtraDeviceListener = new HashSet<>();
 
     private boolean mIsRerun = false;
 
+    private IConfiguration mConfiguration = null;
     private IInvocationContext mContext;
     private List<IMetricCollector> mCollectors = new ArrayList<>();
 
+    /** {@inheritDoc} */
+    @Override
+    public void setConfiguration(IConfiguration config) {
+        mConfiguration = config;
+    }
+
+    /** Gets the {@link IConfiguration} for this test. */
+    public IConfiguration getConfiguration() {
+        return mConfiguration;
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -607,12 +632,6 @@
         return mForceAbi;
     }
 
-    /** Sets the --coverage option for testing. */
-    @VisibleForTesting
-    void setCoverage(boolean coverageEnabled) {
-        mCoverage = coverageEnabled;
-    }
-
     /** Sets the --merge-coverage-measurements option for testing. */
     @VisibleForTesting
     void setMergeCoverageMeasurements(boolean merge) {
@@ -854,16 +873,34 @@
         if (mDebug) {
             mRunner.setDebug(true);
         }
-        if (mCoverage) {
+        if (mConfiguration != null && mConfiguration.getCoverageOptions().isCoverageEnabled()) {
             mRunner.addInstrumentationArg("coverage", "true");
         }
 
-        // Reruns do not create new listeners.
+        // Reruns do not create new listeners or clear coverage measurements.
         if (!mIsRerun) {
             listener = addBugreportListenerIfEnabled(listener);
             listener = addJavaCoverageListenerIfEnabled(listener);
             listener = addNativeCoverageListenerIfEnabled(listener);
 
+            // Clear coverage measurements on the device before running.
+            if (mConfiguration != null
+                    && mConfiguration.getCoverageOptions().isCoverageFlushEnabled()) {
+                CoverageOptions options = mConfiguration.getCoverageOptions();
+
+                if (options.getCoverageToolchains().contains(Toolchain.GCOV)) {
+                    NativeCodeCoverageFlusher flusher =
+                            new NativeCodeCoverageFlusher(mDevice, options.getCoverageProcesses());
+                    flusher.resetCoverage();
+                }
+
+                if (options.getCoverageToolchains().contains(Toolchain.JACOCO)) {
+                    JavaCodeCoverageFlusher flusher =
+                            new JavaCodeCoverageFlusher(mDevice, options.getCoverageProcesses());
+                    flusher.resetCoverage();
+                }
+            }
+
             // TODO: Convert to device-side collectors when possible.
             for (IMetricCollector collector : mCollectors) {
                 if (collector.isDisabled()) {
@@ -922,8 +959,16 @@
      * if this feature is disabled.
      */
     ITestInvocationListener addJavaCoverageListenerIfEnabled(ITestInvocationListener listener) {
-        if (mCoverage) {
-            return new JavaCodeCoverageListener(getDevice(), mMergeCoverageMeasurements, 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;
     }
@@ -933,8 +978,15 @@
      * listener} if this feature is disabled.
      */
     ITestInvocationListener addNativeCoverageListenerIfEnabled(ITestInvocationListener listener) {
-        if (mCoverage) {
-            return new NativeCodeCoverageListener(getDevice(), 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;
     }
@@ -981,7 +1033,8 @@
                 }
             }
             // Don't re-run any completed tests, unless this is a coverage run.
-            if (!mCoverage) {
+            if (mConfiguration != null
+                    && !mConfiguration.getCoverageOptions().isCoverageEnabled()) {
                 expectedTests.removeAll(testTracker.getCurrentRunResults().getCompletedTests());
             }
             rerunTests(expectedTests, listener);
@@ -1013,7 +1066,16 @@
             return;
         }
 
+        if (mNativeCoverageListener != null) {
+            mNativeCoverageListener.setCollectOnTestEnd(false);
+        }
+
         testReRunner.run(listener);
+
+        if (mNativeCoverageListener != null) {
+            mNativeCoverageListener.setCollectOnTestEnd(true);
+            mNativeCoverageListener.logCoverageMeasurements("rerun_merged");
+        }
     }
 
     @VisibleForTesting
diff --git a/src/com/android/tradefed/testtype/JavaCodeCoverageListener.java b/src/com/android/tradefed/testtype/JavaCodeCoverageListener.java
index c58e3c6..bc9929d 100644
--- a/src/com/android/tradefed/testtype/JavaCodeCoverageListener.java
+++ b/src/com/android/tradefed/testtype/JavaCodeCoverageListener.java
@@ -17,6 +17,7 @@
 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;
@@ -25,7 +26,12 @@
 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.collect.ImmutableList;
 
 import org.jacoco.core.tools.ExecFileLoader;
 
@@ -43,18 +49,31 @@
     public static final String COVERAGE_MEASUREMENT_KEY = "coverageFilePath";
 
     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, boolean mergeMeasurements, ITestInvocationListener... listeners) {
+            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
@@ -76,10 +95,7 @@
                 mExecFileLoader.save(mergedMeasurements, false);
 
                 // Save the merged measurement as a test log.
-                try (FileInputStreamSource source =
-                        new FileInputStreamSource(mergedMeasurements, true)) {
-                    testLog("merged_runtime_coverage", LogDataType.COVERAGE, source);
-                }
+                logCoverageMeasurement("merged_runtime_coverage", mergedMeasurements);
             } catch (IOException e) {
                 throw new RuntimeException(e);
             } finally {
@@ -93,37 +109,55 @@
                 super.testRunEnded(elapsedTime, runMetrics);
                 return;
             }
-            String devicePath = devicePathMetric.getMeasurements().getSingleString();
-            if (devicePath == null) {
+            String testCoveragePath = devicePathMetric.getMeasurements().getSingleString();
+            if (testCoveragePath == null) {
                 super.testRunFailed("No coverage measurement.");
                 super.testRunEnded(elapsedTime, runMetrics);
                 return;
             }
 
+            ImmutableList.Builder<String> devicePaths = ImmutableList.builder();
+            devicePaths.add(testCoveragePath);
+
             File coverageFile = null;
             try {
-                coverageFile = mDevice.pullFile(devicePath);
-                verifyNotNull(coverageFile, "Failed to pull the coverage file from %s", devicePath);
+                if (mCoverageOptions.isCoverageFlushEnabled()) {
+                    devicePaths.addAll(mFlusher.forceCoverageFlush());
+                }
 
-                // When merging, load the measurement data. Otherwise log the measurement
-                // immediately.
-                if (mMergeCoverageMeasurements) {
-                    mExecFileLoader.load(coverageFile);
-                } else {
-                    try (FileInputStreamSource source =
-                            new FileInputStreamSource(coverageFile, true)) {
-                        testLog(
-                                mCurrentRunName + "_runtime_coverage",
-                                LogDataType.COVERAGE,
-                                source);
+                for (String devicePath : devicePaths.build()) {
+                    coverageFile = mDevice.pullFile(devicePath);
+                    verifyNotNull(
+                            coverageFile, "Failed to pull the 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);
                     }
                 }
             } catch (DeviceNotAvailableException | IOException e) {
                 throw new RuntimeException(e);
             } finally {
-                FileUtil.deleteFile(coverageFile);
                 super.testRunEnded(elapsedTime, runMetrics);
             }
         }
     }
+
+    private void logCoverageMeasurement(String name, File coverageFile) throws IOException {
+        try (FileInputStreamSource source = new FileInputStreamSource(coverageFile, true)) {
+            testLog(name, LogDataType.COVERAGE, source);
+        }
+    }
 }
diff --git a/src/com/android/tradefed/testtype/NativeCodeCoverageListener.java b/src/com/android/tradefed/testtype/NativeCodeCoverageListener.java
index dca83db..2349c34 100644
--- a/src/com/android/tradefed/testtype/NativeCodeCoverageListener.java
+++ b/src/com/android/tradefed/testtype/NativeCodeCoverageListener.java
@@ -17,6 +17,7 @@
 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;
@@ -25,16 +26,16 @@
 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.base.Splitter;
+import com.google.common.collect.ImmutableList;
 
 import java.io.File;
 import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.HashMap;
 
@@ -44,17 +45,48 @@
  */
 public final class NativeCodeCoverageListener extends ResultForwarder {
 
-    private static final String NATIVE_COVERAGE_DEVICE_PATH = "/data/misc/trace/proc/self/cwd/out";
-    private static final String COVERAGE_FILE_LIST_COMMAND =
-            String.format("find %s -name '*.gcda'", NATIVE_COVERAGE_DEVICE_PATH);
+    private static final String NATIVE_COVERAGE_DEVICE_PATH = "/data/misc/trace";
+    private static final String COVERAGE_TAR_PATH =
+            String.format("%s/coverage.tar.gz", 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 -cvzf %s -T -",
+                    NATIVE_COVERAGE_DEVICE_PATH, COVERAGE_TAR_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
@@ -65,49 +97,61 @@
 
     @Override
     public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
-        // Retrieve the list of .gcda files from the device.  Don't use pullDir since it will
-        // not pull from hidden directories.  Keep the path of the files on the device in the
-        // local directory so that the .gcda files can be mapped to the correct .gcno file.
-        File localDir = null;
         try {
-            localDir = FileUtil.createTempDir("native_coverage");
+            if (mCollectCoverageOnTestEnd) {
+                logCoverageMeasurements(mCurrentRunName);
+            }
+        } finally {
+            super.testRunEnded(elapsedTime, runMetrics);
+        }
+    }
 
-            // Enable abd root on the device, otherwise the list command will fail.
+    /** Pulls native coverage measurements from the device and logs them. */
+    public void logCoverageMeasurements(String runName) {
+        File coverageTarGz = 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.");
-            String findResult = mDevice.executeShellCommand(COVERAGE_FILE_LIST_COMMAND);
 
-            Path devicePathRoot = Paths.get(NATIVE_COVERAGE_DEVICE_PATH);
-            for (String deviceFile : Splitter.on("\n").omitEmptyStrings().split(findResult)) {
-                // Compute the relative path for the device file.
-                Path relativePath = devicePathRoot.relativize(Paths.get(deviceFile));
-                Path localFullPath = localDir.toPath().resolve(relativePath);
-
-                // Create parent directories and pull the file.
-                Files.createDirectories(localFullPath.getParent());
-                verify(
-                        mDevice.pullFile(deviceFile, localFullPath.toFile()),
-                        "Failed to pull the coverage file from %s",
-                        deviceFile);
+            // Flush cross-process coverage.
+            if (mFlushCoverage) {
+                mFlusher.forceCoverageFlush();
             }
 
-            // Zip the contents of the localDir (not including localDir in the path) and log
-            // the resulting file.
-            File coverageZip =
-                    ZipUtil.createZip(
-                            Arrays.asList(localDir.listFiles()),
-                            mCurrentRunName + "_native_runtime_coverage");
+            // Compress coverage measurements on the device before pulling.
+            mDevice.executeShellCommand(ZIP_COVERAGE_FILES_COMMAND);
+            coverageTarGz = mDevice.pullFile(COVERAGE_TAR_PATH);
+            verifyNotNull(coverageTarGz, "Failed to pull the coverage file %s", COVERAGE_TAR_PATH);
+            mDevice.deleteFile(COVERAGE_TAR_PATH);
+
+            coverageZip = convertTarGzToZip(coverageTarGz);
 
             try (FileInputStreamSource source = new FileInputStreamSource(coverageZip, true)) {
-                testLog(
-                        mCurrentRunName + "_native_runtime_coverage",
-                        LogDataType.NATIVE_COVERAGE,
-                        source);
+                testLog(runName + "_native_runtime_coverage", LogDataType.NATIVE_COVERAGE, source);
             }
         } catch (DeviceNotAvailableException | IOException e) {
             throw new RuntimeException(e);
         } finally {
-            FileUtil.recursiveDelete(localDir);
-            super.testRunEnded(elapsedTime, runMetrics);
+            FileUtil.deleteFile(coverageTarGz);
+            FileUtil.deleteFile(coverageZip);
+        }
+    }
+
+    /**
+     * Converts a .tar.gz file to a .zip file.
+     *
+     * @param tarGz the .tar.gz file to convert
+     * @return a .zip file with the same contents
+     * @throws IOException
+     */
+    private File convertTarGzToZip(File tarGz) throws IOException {
+        File untarDir = null;
+        try {
+            untarDir = TarUtil.extractTarGzipToTemp(tarGz, "native_coverage");
+            return ZipUtil.createZip(Arrays.asList(untarDir.listFiles()), "native_coverage");
+        } finally {
+            FileUtil.recursiveDelete(untarDir);
         }
     }
 }
diff --git a/src/com/android/tradefed/testtype/coverage/CoverageOptions.java b/src/com/android/tradefed/testtype/coverage/CoverageOptions.java
new file mode 100644
index 0000000..f1c5527
--- /dev/null
+++ b/src/com/android/tradefed/testtype/coverage/CoverageOptions.java
@@ -0,0 +1,98 @@
+/*
+ * 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.coverage;
+
+import com.android.tradefed.config.Option;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Tradefed object to hold coverage options. */
+public final class CoverageOptions {
+
+    @Option(
+        name = "coverage",
+        description =
+                "Collect code coverage for this test run. Note that the build under test must be a "
+                        + "coverage build or else this will fail."
+    )
+    private boolean mCoverage = false;
+
+    @Option(
+        name = "coverage-toolchain",
+        description =
+                "The coverage toolchains that were used to compile the coverage build to "
+                        + "collect coverage from."
+    )
+    private List<Toolchain> mToolchains = new ArrayList<>();
+
+    public enum Toolchain {
+        GCOV,
+        JACOCO;
+    }
+
+    @Option(
+        name = "coverage-flush",
+        description = "Forces coverage data to be flushed at the end of the test."
+    )
+    private boolean mCoverageFlush = false;
+
+    @Option(
+        name = "coverage-processes",
+        description = "Name of processes to collect coverage data from."
+    )
+    private List<String> mCoverageProcesses = new ArrayList<>();
+
+    /**
+     * Returns whether coverage measurements should be collected from this run.
+     *
+     * @return whether to collect coverage measurements
+     */
+    public boolean isCoverageEnabled() {
+        return mCoverage;
+    }
+
+    /**
+     * Returns the coverage toolchains to collect coverage from.
+     *
+     * @return the toolchains to collect coverage from
+     */
+    public List<Toolchain> getCoverageToolchains() {
+        return ImmutableList.copyOf(mToolchains);
+    }
+
+    /**
+     * Returns whether coverage measurements should be flushed from running processes after the test
+     * has completed.
+     *
+     * @return whether to flush processes for coverage measurements after the test
+     */
+    public boolean isCoverageFlushEnabled() {
+        return mCoverageFlush;
+    }
+
+    /**
+     * Returns the name of processes to flush coverage from after the test has completed.
+     *
+     * @return a {@link List} of process names to flush coverage from after the test
+     */
+    public List<String> getCoverageProcesses() {
+        return ImmutableList.copyOf(mCoverageProcesses);
+    }
+}
diff --git a/src/com/android/tradefed/testtype/suite/AtestRunner.java b/src/com/android/tradefed/testtype/suite/AtestRunner.java
index 860c498..4be6e2a 100644
--- a/src/com/android/tradefed/testtype/suite/AtestRunner.java
+++ b/src/com/android/tradefed/testtype/suite/AtestRunner.java
@@ -25,13 +25,16 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.SubprocessResultsReporter;
 import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.InstrumentationTest;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.ITestFilterReceiver;
+import java.io.File;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -77,6 +80,36 @@
     )
     private List<String> mIncludeFilters = new ArrayList<>();
 
+    @Option(
+        name = "tf-config-path",
+        description =
+                "Allows to run a specific TF configuration path."
+    )
+    private List<String> mTfConfigPaths = new ArrayList<>();
+
+    @Option(
+        name = "module-config-path",
+        description =
+                "Allows to run a specific module configuration path."
+    )
+    private List<File> mModuleConfigPaths = new ArrayList<>();
+
+    @Override
+    public LinkedHashMap<String, IConfiguration> loadingStrategy(Set<IAbi> abis,
+        List<File> testsDirs,
+        String suitePrefix, String suiteTag) {
+        if (mTfConfigPaths.isEmpty() && mModuleConfigPaths.isEmpty()) {
+            return super.loadingStrategy(abis, testsDirs, suitePrefix, suiteTag);
+        }
+        LinkedHashMap<String, IConfiguration> loadedConfigs = new LinkedHashMap<>();
+        loadedConfigs.putAll(
+                getModuleLoader().loadTfConfigsFromSpecifiedPaths(mTfConfigPaths, abis, suiteTag));
+        loadedConfigs.putAll(
+                getModuleLoader().loadConfigsFromSpecifiedPaths(
+                        mModuleConfigPaths, abis, suiteTag));
+        return loadedConfigs;
+    }
+
     @Override
     public LinkedHashMap<String, IConfiguration> loadTests() {
         // atest only needs to run on primary or specified abi.
diff --git a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
index e66b850..2e92d70 100644
--- a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
@@ -26,7 +26,10 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.suite.params.IModuleParameter;
 import com.android.tradefed.testtype.suite.params.ModuleParameters;
+import com.android.tradefed.testtype.suite.params.ModuleParametersHelper;
+import com.android.tradefed.testtype.suite.params.NegativeHandler;
 import com.android.tradefed.util.ArrayUtil;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -48,6 +51,7 @@
     public static final String INCLUDE_FILTER_OPTION = "include-filter";
     public static final String EXCLUDE_FILTER_OPTION = "exclude-filter";
     public static final String MODULE_OPTION = "module";
+    public static final char MODULE_OPTION_SHORT_NAME = 'm';
     public static final String TEST_ARG_OPTION = "test-arg";
     public static final String TEST_OPTION = "test";
     public static final char TEST_OPTION_SHORT_NAME = 't';
@@ -70,7 +74,7 @@
 
     @Option(
         name = MODULE_OPTION,
-        shortName = 'm',
+        shortName = MODULE_OPTION_SHORT_NAME,
         description = "the test module to run. Only works for configuration in the tests dir.",
         importance = Importance.IF_UNSET
     )
@@ -168,6 +172,7 @@
     private SuiteModuleLoader mModuleRepo;
     private Map<String, List<SuiteTestFilter>> mIncludeFiltersParsed = new HashMap<>();
     private Map<String, List<SuiteTestFilter>> mExcludeFiltersParsed = new HashMap<>();
+    private List<File> mConfigPaths = new ArrayList<>();
 
     /** {@inheritDoc} */
     @Override
@@ -230,6 +235,12 @@
     public LinkedHashMap<String, IConfiguration> loadingStrategy(
             Set<IAbi> abis, List<File> testsDirs, String suitePrefix, String suiteTag) {
         LinkedHashMap<String, IConfiguration> loadedConfigs = new LinkedHashMap<>();
+        // Load and return directly the specific config files.
+        if (!mConfigPaths.isEmpty()) {
+            CLog.d("Loading the specified configs and skip loading from the resources.");
+            return getModuleLoader().loadConfigsFromSpecifiedPaths(mConfigPaths, abis, suiteTag);
+        }
+
         // Load configs that are part of the resources
         if (!mSkipJarLoading) {
             loadedConfigs.putAll(
@@ -295,6 +306,11 @@
         mModuleArgs.addAll(moduleArgs);
     }
 
+    /** Clear the stored module args out */
+    void clearModuleArgs() {
+        mModuleArgs.clear();
+    }
+
     /** Add config patterns */
     public void addConfigPatterns(List<String> patterns) {
         mConfigPatterns.addAll(patterns);
@@ -325,43 +341,64 @@
      * @throws FileNotFoundException if any file is not found.
      */
     protected void setupFilters(File testsDir) throws FileNotFoundException {
-        if (mModuleName != null) {
-            // If this option (-m / --module) is set only the matching unique module should run.
-            Set<File> modules =
-                    SuiteModuleLoader.getModuleNamesMatching(
-                            testsDir, mSuitePrefix, String.format(".*%s.*.config", mModuleName));
-            // If multiple modules match, do exact match.
-            if (modules.size() > 1) {
-                Set<File> newModules = new HashSet<>();
-                String exactModuleName = String.format("%s.config", mModuleName);
-                for (File module : modules) {
-                    if (module.getName().equals(exactModuleName)) {
-                        newModules.add(module);
-                        modules = newModules;
-                        break;
-                    }
+        if (mModuleName == null) {
+            if (mTestName != null) {
+                throw new IllegalArgumentException(
+                        "Test name given without module name. Add --module <module-name>");
+            }
+            return;
+        }
+        // If this option (-m / --module) is set only the matching unique module should run.
+        Set<File> modules =
+                SuiteModuleLoader.getModuleNamesMatching(
+                        testsDir, mSuitePrefix, String.format(".*%s.*.config", mModuleName));
+        // If multiple modules match, do exact match.
+        if (modules.size() > 1) {
+            Set<File> newModules = new HashSet<>();
+            String exactModuleName = String.format("%s.config", mModuleName);
+            for (File module : modules) {
+                if (module.getName().equals(exactModuleName)) {
+                    newModules.add(module);
+                    modules = newModules;
+                    break;
                 }
             }
-            if (modules.size() == 0) {
-                throw new IllegalArgumentException(
-                        String.format("No modules found matching %s", mModuleName));
-            } else if (modules.size() > 1) {
-                throw new IllegalArgumentException(
-                        String.format(
-                                "Multiple modules found matching %s:\n%s\nWhich one did you "
-                                        + "mean?\n",
-                                mModuleName, ArrayUtil.join("\n", modules)));
-            } else {
-                File mod = modules.iterator().next();
-                String moduleName = mod.getName().replace(".config", "");
-                checkFilters(mIncludeFilters, moduleName);
-                checkFilters(mExcludeFilters, moduleName);
-                mIncludeFilters.add(
-                        new SuiteTestFilter(getRequestedAbi(), moduleName, mTestName).toString());
-            }
-        } else if (mTestName != null) {
+        }
+        if (modules.size() == 0) {
             throw new IllegalArgumentException(
-                    "Test name given without module name. Add --module <module-name>");
+                    String.format("No modules found matching %s", mModuleName));
+        } else if (modules.size() > 1) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "Multiple modules found matching %s:\n%s\nWhich one did you "
+                                    + "mean?\n",
+                            mModuleName, ArrayUtil.join("\n", modules)));
+        } else {
+            File mod = modules.iterator().next();
+            String moduleName = mod.getName().replace(".config", "");
+            checkFilters(mIncludeFilters, moduleName);
+            checkFilters(mExcludeFilters, moduleName);
+            mIncludeFilters.add(
+                    new SuiteTestFilter(getRequestedAbi(), moduleName, mTestName).toString());
+            // Create the matching filters for the parameterized version of it if needed.
+            if (mEnableParameter) {
+                for (ModuleParameters param : ModuleParameters.values()) {
+                    IModuleParameter moduleParam =
+                            ModuleParametersHelper.getParameterHandler(param, false);
+                    if (moduleParam == null) {
+                        continue;
+                    }
+                    if (moduleParam instanceof NegativeHandler) {
+                        continue;
+                    }
+                    String paramModuleName =
+                            String.format(
+                                    "%s[%s]", moduleName, moduleParam.getParameterIdentifier());
+                    mIncludeFilters.add(
+                            new SuiteTestFilter(getRequestedAbi(), paramModuleName, mTestName)
+                                    .toString());
+                }
+            }
         }
     }
 
@@ -376,6 +413,21 @@
         mExcludeFiltersParsed.clear();
     }
 
+    /**
+     * Add the config path for {@link SuiteModuleLoader} to limit the search loading
+     * configurations.
+     *
+     * @param configPath A {@code File} with the absolute path of the configuration.
+     */
+    void addConfigPaths(File configPath) {
+        mConfigPaths.add(configPath);
+    }
+
+    /** Clear the stored config paths out. */
+    void clearConfigPaths() {
+        mConfigPaths.clear();
+    }
+
     /* Helper method designed to remove filters in a list not applicable to the given module */
     private static void checkFilters(Set<String> filters, String moduleName) {
         Set<String> cleanedFilters = new HashSet<String>();
diff --git a/src/com/android/tradefed/testtype/suite/ITestSuite.java b/src/com/android/tradefed/testtype/suite/ITestSuite.java
index a65b8fc..e979820 100644
--- a/src/com/android/tradefed/testtype/suite/ITestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/ITestSuite.java
@@ -411,6 +411,12 @@
                 continue;
             }
             filterPreparers(config.getValue(), mAllowedPreparers);
+
+            // Copy the CoverageOptions from the main configuration to the module configuration.
+            if (mMainConfiguration != null) {
+                config.getValue().setCoverageOptions(mMainConfiguration.getCoverageOptions());
+            }
+
             filteredConfig.put(config.getKey(), config.getValue());
         }
         runConfig.clear();
diff --git a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
index 78d7b2f..ff1e948 100644
--- a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
+++ b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
@@ -110,6 +110,20 @@
         mExcludedModuleParameters = excludedParams;
     }
 
+    /** Main loading of configurations, looking into the specified files */
+    public LinkedHashMap<String, IConfiguration> loadConfigsFromSpecifiedPaths(
+            List<File> listConfigFiles,
+            Set<IAbi> abis,
+            String suiteTag) {
+        LinkedHashMap<String, IConfiguration> toRun = new LinkedHashMap<>();
+        for (File configFile : listConfigFiles) {
+            toRun.putAll(
+                    loadOneConfig(
+                            configFile.getName(), configFile.getAbsolutePath(), abis, suiteTag));
+        }
+        return toRun;
+    }
+
     /** Main loading of configurations, looking into a folder */
     public LinkedHashMap<String, IConfiguration> loadConfigsFromDirectory(
             List<File> testsDirs,
@@ -123,11 +137,7 @@
                 ConfigurationUtil.getConfigNamesFileFromDirs(suitePrefix, testsDirs, patterns));
         // Ensure stable initial order of configurations.
         Collections.sort(listConfigFiles);
-        for (File configFile : listConfigFiles) {
-            toRun.putAll(
-                    loadOneConfig(
-                            configFile.getName(), configFile.getAbsolutePath(), abis, suiteTag));
-        }
+        toRun.putAll(loadConfigsFromSpecifiedPaths(listConfigFiles, abis, suiteTag));
         return toRun;
     }
 
@@ -143,6 +153,16 @@
         List<String> configs = configFactory.getConfigList(suitePrefix, false);
         // Sort configs to ensure they are always evaluated and added in the same order.
         Collections.sort(configs);
+        toRun.putAll(loadTfConfigsFromSpecifiedPaths(configs, abis, suiteTag));
+        return toRun;
+    }
+
+    /** Main loading of configurations, looking into the specified resources on the classpath. */
+    public LinkedHashMap<String, IConfiguration> loadTfConfigsFromSpecifiedPaths(
+        List<String> configs,
+        Set<IAbi> abis,
+        String suiteTag) {
+        LinkedHashMap<String, IConfiguration> toRun = new LinkedHashMap<>();
         for (String configName : configs) {
             toRun.putAll(loadOneConfig(configName, configName, abis, suiteTag));
         }
@@ -156,17 +176,16 @@
      *
      * @param test The {@link IRemoteTest} that is being considered.
      * @param abi The Abi we are currently working on.
-     * @param name The name of the module.
+     * @param moduleId The id of the module (usually abi + module name).
      * @param includeFilters The formatted and parsed include filters.
      * @param excludeFilters The formatted and parsed exclude filters.
      */
     public void addFiltersToTest(
             IRemoteTest test,
             IAbi abi,
-            String name,
+            String moduleId,
             Map<String, List<SuiteTestFilter>> includeFilters,
             Map<String, List<SuiteTestFilter>> excludeFilters) {
-        String moduleId = AbiUtils.createId(abi.getName(), name);
         if (!(test instanceof ITestFilterReceiver)) {
             CLog.e("Test in module %s does not implement ITestFilterReceiver.", moduleId);
             return;
@@ -174,10 +193,10 @@
         List<SuiteTestFilter> mdIncludes = getFilterList(includeFilters, moduleId);
         List<SuiteTestFilter> mdExcludes = getFilterList(excludeFilters, moduleId);
         if (!mdIncludes.isEmpty()) {
-            addTestIncludes((ITestFilterReceiver) test, mdIncludes, name);
+            addTestIncludes((ITestFilterReceiver) test, mdIncludes, moduleId);
         }
         if (!mdExcludes.isEmpty()) {
-            addTestExcludes((ITestFilterReceiver) test, mdExcludes, name);
+            addTestExcludes((ITestFilterReceiver) test, mdExcludes, moduleId);
         }
     }
 
@@ -402,9 +421,11 @@
     }
 
     private void addTestIncludes(
-            ITestFilterReceiver test, List<SuiteTestFilter> includes, String name) {
+            ITestFilterReceiver test, List<SuiteTestFilter> includes, String moduleId) {
         if (test instanceof ITestFileFilterReceiver) {
-            File includeFile = createFilterFile(name, ".include", includes);
+            // module id can contain spaces, avoid them for file names.
+            String escapedFileName = moduleId.replaceAll(" ", "_");
+            File includeFile = createFilterFile(escapedFileName, ".include", includes);
             ((ITestFileFilterReceiver) test).setIncludeTestFile(includeFile);
         } else {
             // add test includes one at a time
@@ -561,7 +582,7 @@
      *
      * @param name The base name of the module
      * @param id The base id name of the module.
-     * @param fullId The full id of the module.
+     * @param fullId The full id of the module (usually abi + module name + parameters)
      * @param config The module configuration.
      * @param abi The abi of the module.
      * @throws ConfigurationException
@@ -595,7 +616,7 @@
             if (mTestOptions.containsKey(className)) {
                 config.injectOptionValues(mTestOptions.get(className));
             }
-            addFiltersToTest(test, abi, name, mIncludeFilters, mExcludeFilters);
+            addFiltersToTest(test, abi, fullId, mIncludeFilters, mExcludeFilters);
             if (test instanceof IAbiReceiver) {
                 ((IAbiReceiver) test).setAbi(abi);
             }
diff --git a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
index c0dc16a..d32a1fc 100644
--- a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
+++ b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
@@ -18,17 +18,22 @@
 import com.android.tradefed.config.ConfigurationDescriptor;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.Option;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.util.testmapping.TestInfo;
 import com.android.tradefed.util.testmapping.TestMapping;
 import com.android.tradefed.util.testmapping.TestOption;
+import com.google.common.annotations.VisibleForTesting;
 
+import java.io.File;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
+
 /**
  * Implementation of {@link BaseTestSuite} to run tests specified by option include-filter, or
  * TEST_MAPPING files from build, as a suite.
@@ -56,6 +61,26 @@
     )
     private Set<String> mKeywords = new HashSet<>();
 
+    @Option(
+        name = "force-test-mapping-module",
+        description =
+                "Run the specified tests only. The tests loaded from all TEST_MAPPING files in "
+                        + "the source code will be filtered again to force run the specified tests."
+    )
+    private Set<String> mTestModulesForced = new HashSet<>();
+
+    @Option(
+        name = "test-mapping-path",
+        description = "Run tests according to the test mapping path."
+    )
+    private List<String> mTestMappingPaths = new ArrayList<>();
+
+    @Option(
+        name = "use-test-mapping-path",
+        description = "Whether or not to run tests based on the given test mapping path."
+    )
+    private boolean mUseTestMappingPath = false;
+
     /** Special definition in the test mapping structure. */
     private static final String TEST_MAPPING_INCLUDE_FILTER = "include-filter";
 
@@ -78,10 +103,10 @@
      */
     @Override
     public LinkedHashMap<String, IConfiguration> loadTests() {
-        // Map between test names and a list of test sources for each test.
-        Map<String, List<String>> testsInTestMapping = new HashMap<>();
-
         Set<String> includeFilter = getIncludeFilter();
+        // Name of the tests
+        Set<String> testNames = new HashSet<>();
+        Set<TestInfo> testInfosToRun = new HashSet<>();
         if (mTestGroup == null && includeFilter.isEmpty()) {
             throw new RuntimeException(
                     "At least one of the options, --test-mapping-test-group or --include-filter, "
@@ -91,95 +116,204 @@
             throw new RuntimeException(
                     "Must specify --test-mapping-test-group when applying --test-mapping-keyword.");
         }
+        if (mTestGroup == null && !mTestModulesForced.isEmpty()) {
+            throw new RuntimeException(
+                    "Must specify --test-mapping-test-group when applying "
+                            + "--force-test-mapping-module.");
+        }
         if (mTestGroup != null && !includeFilter.isEmpty()) {
             throw new RuntimeException(
                     "If options --test-mapping-test-group is set, option --include-filter should "
                             + "not be set.");
         }
+        if (!includeFilter.isEmpty() && !mTestMappingPaths.isEmpty()) {
+            throw new RuntimeException(
+                    "If option --include-filter is set, option --test-mapping-path should "
+                            + "not be set.");
+        }
 
         if (mTestGroup != null) {
-            Set<TestInfo> testsToRun =
+            if (!mTestMappingPaths.isEmpty()) {
+                TestMapping.setTestMappingPaths(mTestMappingPaths);
+            }
+            testInfosToRun =
                     TestMapping.getTests(
                             getBuildInfo(), mTestGroup, getPrioritizeHostConfig(), mKeywords);
-            if (testsToRun.isEmpty()) {
+            if (!mTestModulesForced.isEmpty()) {
+                CLog.i("Filtering tests for the given names: %s", mTestModulesForced);
+                testInfosToRun =
+                        testInfosToRun
+                                .stream()
+                                .filter(testInfo -> mTestModulesForced.contains(testInfo.getName()))
+                                .collect(Collectors.toSet());
+            }
+            if (testInfosToRun.isEmpty()) {
                 throw new RuntimeException(
                         String.format("No test found for the given group: %s.", mTestGroup));
             }
-
-            // Name of the tests
-            Set<String> testNames = new HashSet<>();
-
-            Set<String> mappingIncludeFilters = new HashSet<>();
-            Set<String> mappingExcludeFilters = new HashSet<>();
-
-            // module-arg options compiled from test options for each test.
-            Set<String> moduleArgs = new HashSet<>();
-            for (TestInfo test : testsToRun) {
-                boolean hasIncludeFilters = false;
-                for (TestOption option : test.getOptions()) {
-                    switch (option.getName()) {
-                            // Handle include and exclude filter at the suite level to hide each
-                            // test runner specific implementation and option names related to filtering
-                        case TEST_MAPPING_INCLUDE_FILTER:
-                            hasIncludeFilters = true;
-                            mappingIncludeFilters.add(
-                                    String.format("%s %s", test.getName(), option.getValue()));
-                            break;
-                        case TEST_MAPPING_EXCLUDE_FILTER:
-                            mappingExcludeFilters.add(
-                                    String.format("%s %s", test.getName(), option.getValue()));
-                            break;
-                        default:
-                            String moduleArg =
-                                    String.format("%s:%s", test.getName(), option.getName());
-                            if (option.getValue() != null && !option.getValue().isEmpty()) {
-                                moduleArg = String.format("%s:%s", moduleArg, option.getValue());
-                            }
-                            moduleArgs.add(moduleArg);
-                            break;
-                    }
-                }
-                if (!hasIncludeFilters) {
-                    testNames.add(test.getName());
-                }
+            for (TestInfo testInfo : testInfosToRun) {
+                testNames.add(testInfo.getName());
             }
-
-            if (mappingIncludeFilters.isEmpty()) {
-                setIncludeFilter(testNames);
-            } else {
-                mappingIncludeFilters.addAll(testNames);
-                setIncludeFilter(mappingIncludeFilters);
-            }
-            if (!mappingExcludeFilters.isEmpty()) {
-                setExcludeFilter(mappingExcludeFilters);
-            }
-            addModuleArgs(moduleArgs);
-
-            for (TestInfo test : testsToRun) {
-                List<String> testSources = null;
-                // TODO(b/117880789): tests may not be grouped by name once that bug is fixed.
-                // Update the dictionary with better keys.
-                if (testsInTestMapping.containsKey(test.getName())) {
-                    testSources = testsInTestMapping.get(test.toString());
-                } else {
-                    testSources = new ArrayList<String>();
-                    testsInTestMapping.put(test.getName(), testSources);
-                }
-                testSources.addAll(test.getSources());
-            }
+            setIncludeFilter(testNames);
         }
 
+        // load all the configurations with include-filter injected.
         LinkedHashMap<String, IConfiguration> testConfigs = super.loadTests();
+
+        // Create and inject individual tests by calling super.loadTests() with each test info.
         for (Map.Entry<String, IConfiguration> entry : testConfigs.entrySet()) {
+            List<IRemoteTest> allTests = new ArrayList<>();
+            IConfiguration moduleConfig = entry.getValue();
             ConfigurationDescriptor configDescriptor =
-                    entry.getValue().getConfigurationDescription();
-            if (testsInTestMapping.containsKey(configDescriptor.getModuleName())) {
-                configDescriptor.addMetaData(
-                        TestMapping.TEST_SOURCES,
-                        testsInTestMapping.get(configDescriptor.getModuleName()));
+                    moduleConfig.getConfigurationDescription();
+            String moduleName = configDescriptor.getModuleName();
+            String configPath = moduleConfig.getName();
+            Set<TestInfo> testInfos = getTestInfos(testInfosToRun, moduleName);
+            allTests.addAll(
+                    createIndividualTests(
+                            testInfos, configPath));
+            if (!allTests.isEmpty()) {
+                // Set back to IConfiguration only if IRemoteTests are created.
+                moduleConfig.setTests(allTests);
+                // Set test sources to ConfigurationDescriptor.
+                List<String> testSources = getTestSources(testInfos);
+                configDescriptor.addMetaData(TestMapping.TEST_SOURCES, testSources);
+            }
+        }
+        return testConfigs;
+    }
+
+    /**
+     * Create individual tests with test infos for a module.
+     *
+     * @param testInfos A {@code Set<TestInfo>} containing multiple test options.
+     * @param configPath A {@code String} of configuration path.
+     * @return The {@link List<IRemoteTest>} that are injected with the test options.
+     */
+    @VisibleForTesting
+    List<IRemoteTest> createIndividualTests(Set<TestInfo> testInfos, String configPath) {
+        List<IRemoteTest> tests = new ArrayList<>();
+        if (configPath == null) {
+            throw new RuntimeException(String.format("Configuration path is null."));
+        }
+        File configFie = new File(configPath);
+        if (!configFie.exists()) {
+            throw new RuntimeException(
+                    String.format("Configuration path: %s doesn't exits.", configPath));
+        }
+        // De-duplicate test infos so that there won't be duplicate test options.
+        testInfos = dedupTestInfos(testInfos);
+        for (TestInfo testInfo : testInfos) {
+            // Clean up all the test options injected in SuiteModuleLoader.
+            super.cleanUpSuiteSetup();
+            super.clearModuleArgs();
+            clearConfigPaths();
+            // Set config path to BaseTestSuite to limit the search.
+            addConfigPaths(configFie);
+            // Inject the test options from each test info to SuiteModuleLoader.
+            parseOptions(testInfo);
+            LinkedHashMap<String, IConfiguration> config = super.loadTests();
+            for (Map.Entry<String, IConfiguration> entry : config.entrySet()) {
+                tests.addAll(entry.getValue().getTests());
+            }
+        }
+        return tests;
+    }
+
+    /**
+     * Get a list of path of TEST_MAPPING for a module.
+     *
+     * @param testInfos A {@code Set<TestInfo>} containing multiple test options.
+     * @return A {@code List<String>} of TEST_MAPPING path.
+     */
+    @VisibleForTesting
+    List<String> getTestSources(Set<TestInfo> testInfos) {
+        List<String> testSources = new ArrayList<>();
+        for (TestInfo testInfo : testInfos) {
+            testSources.addAll(testInfo.getSources());
+        }
+        return testSources;
+    }
+
+    /**
+     * Parse the test options for the test info.
+     *
+     * @param testInfo A {@code Set<TestInfo>} containing multiple test options.
+     */
+    @VisibleForTesting
+    void parseOptions(TestInfo testInfo) {
+        Set<String> mappingIncludeFilters = new HashSet<>();
+        Set<String> mappingExcludeFilters = new HashSet<>();
+        // module-arg options compiled from test options for each test.
+        Set<String> moduleArgs = new HashSet<>();
+        Set<String> testNames = new HashSet<>();
+        for (TestOption option : testInfo.getOptions()) {
+            switch (option.getName()) {
+                // Handle include and exclude filter at the suite level to hide each
+                // test runner specific implementation and option names related to filtering
+                case TEST_MAPPING_INCLUDE_FILTER:
+                    mappingIncludeFilters.add(
+                            String.format("%s %s", testInfo.getName(), option.getValue()));
+                    break;
+                case TEST_MAPPING_EXCLUDE_FILTER:
+                    mappingExcludeFilters.add(
+                            String.format("%s %s", testInfo.getName(), option.getValue()));
+                    break;
+                default:
+                    String moduleArg =
+                            String.format("%s:%s", testInfo.getName(), option.getName());
+                    if (option.getValue() != null && !option.getValue().isEmpty()) {
+                        moduleArg = String.format("%s:%s", moduleArg, option.getValue());
+                    }
+                    moduleArgs.add(moduleArg);
+                    break;
             }
         }
 
-        return testConfigs;
+        if (mappingIncludeFilters.isEmpty()) {
+            testNames.add(testInfo.getName());
+            setIncludeFilter(testNames);
+        } else {
+            setIncludeFilter(mappingIncludeFilters);
+        }
+        if (!mappingExcludeFilters.isEmpty()) {
+            setExcludeFilter(mappingExcludeFilters);
+        }
+        addModuleArgs(moduleArgs);
+    }
+
+    /**
+     * De-duplicate test infos with the same test options.
+     *
+     * @param testInfos A {@code Set<TestInfo>} containing multiple test options.
+     * @return A {@code Set<TestInfo>} of tests without duplicated test options.
+     */
+    @VisibleForTesting
+    Set<TestInfo> dedupTestInfos(Set<TestInfo> testInfos) {
+        Set<String> nameOptions = new HashSet<>();
+        Set<TestInfo> dedupTestInfos = new HashSet<>();
+        for (TestInfo testInfo : testInfos) {
+            String nameOption = testInfo.getName() + testInfo.getOptions().toString();
+            if (!nameOptions.contains(nameOption)) {
+                dedupTestInfos.add(testInfo);
+                nameOptions.add(nameOption);
+            }
+        }
+        return dedupTestInfos;
+    }
+
+    /**
+     * Get the test infos for the given module name.
+     *
+     * @param testInfos A {@code Set<TestInfo>} containing multiple test options.
+     * @param moduleName A {@code String} name of a test module.
+     * @return A {@code Set<TestInfo>} of tests for a module.
+     */
+    @VisibleForTesting
+    Set<TestInfo> getTestInfos(Set<TestInfo> testInfos, String moduleName) {
+        return testInfos
+                .stream()
+                .filter(testInfo -> moduleName.equals(testInfo.getName()))
+                .collect(Collectors.toSet());
     }
 }
diff --git a/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java b/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java
index 3206494..8dfbccb 100644
--- a/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java
+++ b/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java
@@ -75,6 +75,13 @@
                             + "and \"not_executed\".")
     private RetryType mRetryType = null;
 
+    @Option(
+        name = BaseTestSuite.MODULE_OPTION,
+        shortName = BaseTestSuite.MODULE_OPTION_SHORT_NAME,
+        description = "the test module to run. Only works for configuration in the tests dir."
+    )
+    private String mModuleName = null;
+
     /**
      * It's possible to add extra exclusion from the rerun. But these tests will not change their
      * state.
@@ -224,6 +231,22 @@
             types.add(mRetryType);
         }
 
+        // Expand the --module option in case no abi is specified.
+        Set<String> expandedModuleOption = new HashSet<>();
+        if (mModuleName != null) {
+            SuiteTestFilter moduleFilter = SuiteTestFilter.createFrom(mModuleName);
+            expandedModuleOption.add(mModuleName);
+            if (moduleFilter.getAbi() == null) {
+                Set<String> abis = AbiUtils.getAbisSupportedByCompatibility();
+                for (String abi : abis) {
+                    SuiteTestFilter namingFilter =
+                            new SuiteTestFilter(
+                                    abi, moduleFilter.getName(), moduleFilter.getTest());
+                    expandedModuleOption.add(namingFilter.toString());
+                }
+            }
+        }
+
         // Expand the exclude-filter in case no abi is specified.
         Set<String> extendedExcludeRetryFilters = new HashSet<>();
         for (String excludeFilter : mExcludeFilters) {
@@ -245,6 +268,8 @@
         for (TestRunResult moduleResult : results.getMergedTestRunResults()) {
             // If the module is explicitly excluded from retries, preserve the original results.
             if (!extendedExcludeRetryFilters.contains(moduleResult.getName())
+                    && (expandedModuleOption.isEmpty()
+                            || expandedModuleOption.contains(moduleResult.getName()))
                     && RetryResultHelper.shouldRunModule(moduleResult, types)) {
                 if (types.contains(RetryType.NOT_EXECUTED)) {
                     // Clear the run failure since we are attempting to rerun all non-executed
diff --git a/src/com/android/tradefed/util/JavaCodeCoverageFlusher.java b/src/com/android/tradefed/util/JavaCodeCoverageFlusher.java
new file mode 100644
index 0000000..88a562a
--- /dev/null
+++ b/src/com/android/tradefed/util/JavaCodeCoverageFlusher.java
@@ -0,0 +1,146 @@
+/*
+ * 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.util;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A utility class that resets and forces a flush of Java code coverage measurements from processes
+ * running on the device.
+ */
+public class JavaCodeCoverageFlusher {
+
+    private static final String COVERAGE_RESET_FORMAT =
+            "am attach-agent %s /system/lib/libdumpcoverage.so=reset";
+    private static final String COVERAGE_FLUSH_FORMAT =
+            "am attach-agent %s /system/lib/libdumpcoverage.so=dump:%s";
+    private static final String PACKAGE_PREFIX = "package:";
+
+    private final ITestDevice mDevice;
+    private final List<String> mProcessNames;
+
+    public JavaCodeCoverageFlusher(ITestDevice device, List<String> processNames) {
+        mDevice = device;
+        mProcessNames = ImmutableList.copyOf(processNames);
+    }
+
+    /** Retrieves the name of all running processes on the device. */
+    private List<String> getAllProcessNames() throws DeviceNotAvailableException {
+        List<ProcessInfo> processes = PsParser.getProcesses(mDevice.executeShellCommand("ps -e"));
+        List<String> names = new ArrayList<>();
+
+        for (ProcessInfo pinfo : processes) {
+            names.add(pinfo.getName());
+        }
+
+        return names;
+    }
+
+    /** Retrieves the set of all installed packages on the device. */
+    private Set<String> getAllPackages() throws DeviceNotAvailableException {
+        Splitter splitter = Splitter.on('\n').trimResults().omitEmptyStrings();
+        String pmListPackages = mDevice.executeShellCommand("pm list packages -a");
+
+        List<String> lines = splitter.splitToList(pmListPackages);
+        ImmutableSet.Builder<String> packages = ImmutableSet.builder();
+
+        // Strip "package:" prefix from the output.
+        for (String line : lines) {
+            if (line.startsWith(PACKAGE_PREFIX)) {
+                line = line.substring(PACKAGE_PREFIX.length());
+            }
+
+            packages.add(line);
+        }
+
+        return packages.build();
+    }
+
+    /**
+     * Resets the Java code coverage counters for the given processes.
+     *
+     * @throws DeviceNotAvailableException
+     */
+    public void resetCoverage() throws DeviceNotAvailableException {
+        List<String> processNames = mProcessNames;
+
+        if (processNames.isEmpty()) {
+            processNames = getAllProcessNames();
+        }
+
+        Set<String> javaPackages = getAllPackages();
+
+        // Attempt to reset the coverage measurements for the given processes.
+        for (String processName :
+                Sets.intersection(javaPackages, ImmutableSet.copyOf(processNames))) {
+            String pid = mDevice.getProcessPid(processName);
+
+            if (pid != null) {
+                mDevice.executeShellCommand(String.format(COVERAGE_RESET_FORMAT, processName));
+            }
+        }
+
+        // Reset coverage for system_server.
+        mDevice.executeShellCommand("cmd coverage reset");
+    }
+
+    /**
+     * Forces a flush of Java coverage data from processes running on the device.
+     *
+     * @return a list of the coverage files generated on the device.
+     * @throws DeviceNotAvailableException
+     */
+    public List<String> forceCoverageFlush() throws DeviceNotAvailableException {
+        List<String> processNames = mProcessNames;
+        if (processNames.isEmpty()) {
+            processNames = getAllProcessNames();
+        }
+
+        List<String> coverageFiles = new ArrayList<>();
+
+        Set<String> javaPackages = getAllPackages();
+
+        // Flush coverage for the given processes, if the process exists and is a Java process.
+        for (String processName :
+                Sets.intersection(javaPackages, ImmutableSet.copyOf(processNames))) {
+            String pid = mDevice.getProcessPid(processName);
+
+            if (pid != null) {
+                String outputPath = String.format("/data/misc/trace/%s-%s.ec", processName, pid);
+                mDevice.executeShellCommand(
+                        String.format(COVERAGE_FLUSH_FORMAT, processName, outputPath));
+                coverageFiles.add(outputPath);
+            }
+        }
+
+        // Flush coverage for system_server.
+        mDevice.executeShellCommand("cmd coverage dump /data/misc/trace/system_server.ec");
+        coverageFiles.add("/data/misc/trace/system_server.ec");
+
+        return coverageFiles;
+    }
+}
diff --git a/src/com/android/tradefed/util/NativeCodeCoverageFlusher.java b/src/com/android/tradefed/util/NativeCodeCoverageFlusher.java
index b53542a..7c0ae3b 100644
--- a/src/com/android/tradefed/util/NativeCodeCoverageFlusher.java
+++ b/src/com/android/tradefed/util/NativeCodeCoverageFlusher.java
@@ -35,18 +35,21 @@
     private static final String CLEAR_NATIVE_COVERAGE_FILES = "rm -rf /data/misc/trace/*";
 
     private final ITestDevice mDevice;
+    private final List<String> mProcessNames;
 
-    public NativeCodeCoverageFlusher(ITestDevice device) {
+    public NativeCodeCoverageFlusher(ITestDevice device, List<String> processNames) {
         mDevice = device;
+        mProcessNames = processNames;
     }
 
     /**
-     * Clears coverage measurements from disk on the device. Device must be in adb root.
+     * Resets native coverage counters for processes running on the device and clears any existing
+     * coverage measurements from disk. Device must be in adb root.
      *
      * @throws DeviceNotAvailableException
      */
-    public void clearCoverageMeasurements() throws DeviceNotAvailableException {
-        checkState(mDevice.isAdbRoot(), "adb root is required to clear coverage files.");
+    public void resetCoverage() throws DeviceNotAvailableException {
+        forceCoverageFlush();
         mDevice.executeShellCommand(CLEAR_NATIVE_COVERAGE_FILES);
     }
 
@@ -54,20 +57,18 @@
      * Forces a flush of native coverage data from processes running on the device. Device must be
      * in adb root.
      *
-     * @param processNames the name of processes to target for flushing; if empty, flushes from all
-     *     running native processes on the device.
      * @throws DeviceNotAvailableException
      */
-    public void forceCoverageFlush(List<String> processNames) throws DeviceNotAvailableException {
+    public void forceCoverageFlush() throws DeviceNotAvailableException {
         checkState(mDevice.isAdbRoot(), "adb root is required to flush native coverage data.");
 
-        if ((processNames == null) || processNames.isEmpty()) {
+        if ((mProcessNames == null) || mProcessNames.isEmpty()) {
             // Use the special pid -1 to trigger a coverage flush of all running processes.
             mDevice.executeShellCommand(String.format(COVERAGE_FLUSH_COMMAND_FORMAT, "-1"));
         } else {
             // Look up the pid of the processes to send them the coverage flush signal.
             StringJoiner pidString = new StringJoiner(" ");
-            for (String processName : processNames) {
+            for (String processName : mProcessNames) {
                 String pid = mDevice.getProcessPid(processName);
                 if (pid == null) {
                     CLog.w("Did not find pid for process \"%s\".", processName);
diff --git a/src/com/android/tradefed/util/TarUtil.java b/src/com/android/tradefed/util/TarUtil.java
index d9dfb2a..672696f 100644
--- a/src/com/android/tradefed/util/TarUtil.java
+++ b/src/com/android/tradefed/util/TarUtil.java
@@ -81,6 +81,15 @@
                     }
                 } else {
                     CLog.i(String.format("Creating output file %s.", outputFile.getAbsolutePath()));
+                    final File parent = outputFile.getParentFile();
+                    if (parent != null && !parent.exists()) {
+                        if (!parent.mkdirs()) {
+                            throw new IOException(
+                                    String.format(
+                                            "Couldn't create directory %s.",
+                                            parent.getAbsolutePath()));
+                        }
+                    }
                     final OutputStream outputFileStream = new FileOutputStream(outputFile);
                     IOUtils.copy(debInputStream, outputFileStream);
                     StreamUtil.close(outputFileStream);
@@ -153,6 +162,33 @@
     }
 
     /**
+     * Untar and ungzip a tar.gz file to a temp directory.
+     *
+     * @param targzFile the tar.gz file to extract.
+     * @param nameHint the prefix for the temp directory.
+     * @return the temp directory.
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    public static File extractTarGzipToTemp(File targzFile, String nameHint)
+            throws FileNotFoundException, IOException {
+        File unGzipDir = null;
+        File unTarDir = null;
+        try {
+            unGzipDir = FileUtil.createTempDir("extractTarGzip");
+            File tarFile = TarUtil.unGzip(targzFile, unGzipDir);
+            unTarDir = FileUtil.createTempDir(nameHint);
+            TarUtil.unTar(tarFile, unTarDir);
+            return unTarDir;
+        } catch (IOException e) {
+            FileUtil.recursiveDelete(unTarDir);
+            throw e;
+        } finally {
+            FileUtil.recursiveDelete(unGzipDir);
+        }
+    }
+
+    /**
      * Helper to extract and log to the reporters a tar gz file and its content
      *
      * @param listener the {@link ITestLogger} where to log the files.
diff --git a/src/com/android/tradefed/util/testmapping/TestMapping.java b/src/com/android/tradefed/util/testmapping/TestMapping.java
index 79da7c2..93419c0 100644
--- a/src/com/android/tradefed/util/testmapping/TestMapping.java
+++ b/src/com/android/tradefed/util/testmapping/TestMapping.java
@@ -34,6 +34,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -42,6 +43,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -63,6 +66,24 @@
     private static final String DISABLED_PRESUBMIT_TESTS_FILE = "disabled-presubmit-tests";
 
     private Map<String, Set<TestInfo>> mTestCollection = null;
+    // Pattern used to identify comments start with "//" or "#" in TEST_MAPPING.
+    private static final Pattern COMMENTS_REGEX = Pattern.compile(
+            "(?m)[\\s\\t]*(//|#).*|(\".*?\")");
+    private static final Set<String> COMMENTS = new HashSet<>(Arrays.asList("#", "//"));
+
+    private static List<String> mTestMappingRelativePaths = new ArrayList<>();
+
+    /**
+     * Set the TEST_MAPPING paths inside of TEST_MAPPINGS_ZIP to limit loading the TEST_MAPPING.
+     *
+     * @param relativePaths A {@code List<String>} of TEST_MAPPING paths relative to
+     *     TEST_MAPPINGS_ZIP.
+     */
+    public static void setTestMappingPaths(List<String> relativePaths) {
+        mTestMappingRelativePaths.clear();
+        mTestMappingRelativePaths.addAll(relativePaths);
+    }
+
 
     /**
      * Constructor to create a {@link TestMapping} object from a path to TEST_MAPPING file.
@@ -75,7 +96,8 @@
         String relativePath = testMappingsDir.relativize(path.getParent()).toString();
         String errorMessage = null;
         try {
-            String content = String.join("", Files.readAllLines(path, StandardCharsets.UTF_8));
+            String content = removeComments(
+                    String.join("\n", Files.readAllLines(path, StandardCharsets.UTF_8)));
             if (content != null) {
                 JSONTokener tokener = new JSONTokener(content);
                 JSONObject root = new JSONObject(tokener);
@@ -139,6 +161,26 @@
     }
 
     /**
+     * Helper to remove comments in a TEST_MAPPING file to valid format. Only "//" and "#" are
+     * regarded as comments.
+     *
+     * @param jsonContent A {@link String} of json which content is from a TEST_MAPPING file.
+     * @return A {@link String} of valid json without comments.
+     */
+    @VisibleForTesting
+    static String removeComments(String jsonContent) {
+        StringBuffer out = new StringBuffer();
+        Matcher matcher = COMMENTS_REGEX.matcher(jsonContent);
+        while (matcher.find()) {
+            if (COMMENTS.contains(matcher.group(1))) {
+                matcher.appendReplacement(out, "");
+            }
+        }
+        matcher.appendTail(out);
+        return out.toString();
+    }
+
+    /**
      * Helper to get all tests set in a TEST_MAPPING file for a given group.
      *
      * @param testGroup A {@link String} of the test group.
@@ -219,6 +261,39 @@
     }
 
     /**
+     * Helper to get all TEST_MAPPING paths relative to TEST_MAPPINGS_ZIP.
+     *
+     * @param testMappingsRootPath The {@link Path} to a test mappings zip path.
+     * @return A {@code Set<Path>} of all the TEST_MAPPING paths relative to TEST_MAPPINGS_ZIP.
+     */
+    @VisibleForTesting
+    static Set<Path> getAllTestMappingPaths(Path testMappingsRootPath) {
+        Set<Path> allTestMappingPaths = new HashSet<>();
+        for (String path : mTestMappingRelativePaths) {
+            boolean hasAdded = false;
+            Path testMappingPath = testMappingsRootPath.resolve(path);
+            // Recursively find the TEST_MAPPING file until reaching to testMappingsRootPath.
+            while (!testMappingPath.equals(testMappingsRootPath)) {
+                if (testMappingPath.resolve(TEST_MAPPING).toFile().exists()) {
+                    hasAdded = true;
+                    CLog.d("Adding TEST_MAPPING path: %s", testMappingPath);
+                    allTestMappingPaths.add(testMappingPath.resolve(TEST_MAPPING));
+                }
+                testMappingPath = testMappingPath.getParent();
+            }
+            if (!hasAdded) {
+                CLog.w("Couldn't find TEST_MAPPING files from %s", path);
+            }
+        }
+        if (allTestMappingPaths.isEmpty()) {
+            throw new RuntimeException(
+                    String.format(
+                            "Couldn't find TEST_MAPPING files from %s", mTestMappingRelativePaths));
+        }
+        return allTestMappingPaths;
+    }
+
+    /**
      * Helper to find all tests in all TEST_MAPPING files. This is needed when a suite run requires
      * to run all tests in TEST_MAPPING files for a given group, e.g., presubmit.
      *
@@ -236,7 +311,12 @@
         try {
             Path testMappingsRootPath = Paths.get(testMappingsDir.getAbsolutePath());
             Set<String> disabledTests = getDisabledTests(testMappingsRootPath, testGroup);
-            stream = Files.walk(testMappingsRootPath, FileVisitOption.FOLLOW_LINKS);
+            if (mTestMappingRelativePaths.isEmpty()) {
+                stream = Files.walk(testMappingsRootPath, FileVisitOption.FOLLOW_LINKS);
+            }
+            else {
+                stream = getAllTestMappingPaths(testMappingsRootPath).stream();
+            }
             stream.filter(path -> path.getFileName().toString().equals(TEST_MAPPING))
                     .forEach(
                             path ->
@@ -260,7 +340,7 @@
             FileUtil.recursiveDelete(testMappingsDir);
         }
 
-        return TestMapping.mergeTests(tests);
+        return tests;
     }
 
     /**
diff --git a/tests/res/testdata/test_mapping_golden1 b/tests/res/testdata/test_mapping_golden1
new file mode 100644
index 0000000..db3998d
--- /dev/null
+++ b/tests/res/testdata/test_mapping_golden1
@@ -0,0 +1,14 @@
+{
+  "presubmit": [
+    {
+      "name": "test1",
+      "host": true,
+      "include-filter": "testClass#testMethod"
+    }
+  ],
+  "imports": [
+    {
+      "path": "path1//path2//path3"
+    }
+  ]
+}
diff --git a/tests/res/testdata/test_mapping_golden2 b/tests/res/testdata/test_mapping_golden2
new file mode 100644
index 0000000..07486c0
--- /dev/null
+++ b/tests/res/testdata/test_mapping_golden2
@@ -0,0 +1,48 @@
+{
+  "presubmit": [
+    {
+      "name": "test1",
+      "host": true
+    },
+    {
+      "name": "suite/stub1"
+    },
+    {
+      "name": "suite/stub2",
+      "keywords": ["key_1"]
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "test2",
+      "options": [
+        {
+          "instrumentation-arg": "annotation=android.platform.test.annotations.Presubmit"
+        }
+      ]
+    },
+    {
+      "name": "instrument",
+      "options": [
+        {
+          "run-name": "some-name"
+        }
+      ]
+    }
+  ],
+  "othertype": [
+    {
+      "name": "test3",
+      "options": [
+        {
+          "just-an-option": ""
+        }
+      ]
+    }
+  ],
+  "imports": [
+    {
+      "path": "folder1//folder2//folder3"
+    }
+  ]
+}
diff --git a/tests/res/testdata/test_mapping_with_comments1 b/tests/res/testdata/test_mapping_with_comments1
new file mode 100644
index 0000000..3f4083f
--- /dev/null
+++ b/tests/res/testdata/test_mapping_with_comments1
@@ -0,0 +1,16 @@
+{#comments1
+  "presubmit": [//comments2 // comments3 # comment4
+  #comments3
+    { #comments4
+      "name": "test1",#comments5
+//comments6
+      "host": true,//comments7
+      "include-filter": "testClass#testMethod" #comment11 // another comments
+    }#comments8
+  ],#comments9 // another comments
+  "imports": [
+    {
+      "path": "path1//path2//path3"#comment12
+    }
+  ]
+}#comments10
diff --git a/tests/res/testdata/test_mapping_with_comments2 b/tests/res/testdata/test_mapping_with_comments2
new file mode 100644
index 0000000..da5a503
--- /dev/null
+++ b/tests/res/testdata/test_mapping_with_comments2
@@ -0,0 +1,49 @@
+{//comment..// another comment # another comment
+  "presubmit": [ #comment //another comment #// another comment
+    { # comment
+      "name": "test1",//comment
+      "host": true#comment
+    },#comment
+    {
+      "name": "suite/stub1" // comment # comment
+    },
+    {
+      "name": "suite/stub2",
+      "keywords": ["key_1"] # comment
+    } # comment
+  ], // comment
+  "postsubmit": [
+    {
+      "name": "test2",
+      "options": [
+        {
+          "instrumentation-arg": "annotation=android.platform.test.annotations.Presubmit" //comment
+        }
+      ]
+    },
+    {
+      "name": "instrument",
+      "options": [
+        {
+          "run-name": "some-name"
+        }
+      ]
+    }
+  ],
+  "othertype": [ // another comment
+    {
+      "name": "test3",
+      "options": [
+        {
+          "just-an-option": ""##comment
+        }////another comment
+      //// another comment...
+      ]
+    }
+  ],
+  "imports": [
+    {
+      "path": "folder1//folder2//folder3"//comment... # another comment // another comment
+    }
+  ]
+}
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 2b855a3..8e6606e 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -136,6 +136,7 @@
 import com.android.tradefed.postprocessor.AggregatePostProcessorTest;
 import com.android.tradefed.postprocessor.AveragePostProcessorTest;
 import com.android.tradefed.postprocessor.BasePostProcessorTest;
+import com.android.tradefed.result.ATestFileSystemLogSaverTest;
 import com.android.tradefed.result.BugreportCollectorTest;
 import com.android.tradefed.result.CollectingTestListenerTest;
 import com.android.tradefed.result.ConsoleResultReporterTest;
@@ -302,6 +303,7 @@
 import com.android.tradefed.util.GCSFileDownloaderTest;
 import com.android.tradefed.util.GoogleApiClientUtilTest;
 import com.android.tradefed.util.HprofAllocSiteParserTest;
+import com.android.tradefed.util.JavaCodeCoverageFlusherTest;
 import com.android.tradefed.util.JUnitXmlParserTest;
 import com.android.tradefed.util.KeyguardControllerStateTest;
 import com.android.tradefed.util.ListInstrumentationParserTest;
@@ -535,6 +537,7 @@
     BasePostProcessorTest.class,
 
     // result
+    ATestFileSystemLogSaverTest.class,
     BugreportCollectorTest.class,
     CollectingTestListenerTest.class,
     ConsoleResultReporterTest.class,
@@ -742,6 +745,7 @@
     GCSFileDownloaderTest.class,
     GoogleApiClientUtilTest.class,
     HprofAllocSiteParserTest.class,
+    JavaCodeCoverageFlusherTest.class,
     JUnitXmlParserTest.class,
     KeyguardControllerStateTest.class,
     LegacySubprocessResultsReporterTest.class,
diff --git a/tests/src/com/android/tradefed/device/TestDeviceTest.java b/tests/src/com/android/tradefed/device/TestDeviceTest.java
index 41b76cd..81916ef 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceTest.java
@@ -2756,6 +2756,20 @@
     }
 
     /**
+     * Test that remount vendor works as expected on a device not supporting dm verity
+     *
+     * @throws Exception
+     */
+    public void testRemountVendor_verityUnsupported() throws Exception {
+        injectSystemProperty("partition.vendor.verified", "");
+        setExecuteAdbCommandExpectations(new CommandResult(CommandStatus.SUCCESS), "remount");
+        EasyMock.expect(mMockStateMonitor.waitForDeviceAvailable()).andReturn(mMockIDevice);
+        replayMocks();
+        mTestDevice.remountVendorWritable();
+        verifyMocks();
+    }
+
+    /**
      * Test that remount works as expected on a device supporting dm verity v1
      * @throws Exception
      */
@@ -2770,6 +2784,22 @@
         mTestDevice.remountSystemWritable();
         verifyMocks();
     }
+    /**
+     * Test that remount vendor works as expected on a device supporting dm verity v1
+     *
+     * @throws Exception
+     */
+    public void testRemountVendor_veritySupportedV1() throws Exception {
+        injectSystemProperty("partition.vendor.verified", "1");
+        setExecuteAdbCommandExpectations(
+                new CommandResult(CommandStatus.SUCCESS), "disable-verity");
+        setRebootExpectations();
+        setExecuteAdbCommandExpectations(new CommandResult(CommandStatus.SUCCESS), "remount");
+        EasyMock.expect(mMockStateMonitor.waitForDeviceAvailable()).andReturn(mMockIDevice);
+        replayMocks();
+        mTestDevice.remountVendorWritable();
+        verifyMocks();
+    }
 
     /**
      * Test that remount works as expected on a device supporting dm verity v2
@@ -2788,6 +2818,23 @@
     }
 
     /**
+     * Test that remount vendor works as expected on a device supporting dm verity v2
+     *
+     * @throws Exception
+     */
+    public void testRemountVendor_veritySupportedV2() throws Exception {
+        injectSystemProperty("partition.vendor.verified", "2");
+        setExecuteAdbCommandExpectations(
+                new CommandResult(CommandStatus.SUCCESS), "disable-verity");
+        setRebootExpectations();
+        setExecuteAdbCommandExpectations(new CommandResult(CommandStatus.SUCCESS), "remount");
+        EasyMock.expect(mMockStateMonitor.waitForDeviceAvailable()).andReturn(mMockIDevice);
+        replayMocks();
+        mTestDevice.remountVendorWritable();
+        verifyMocks();
+    }
+
+    /**
      * Test that remount works as expected on a device supporting dm verity but with unknown version
      * @throws Exception
      */
@@ -2804,6 +2851,24 @@
     }
 
     /**
+     * Test that remount vendor works as expected on a device supporting dm verity but with unknown
+     * version
+     *
+     * @throws Exception
+     */
+    public void testRemountVendor_veritySupportedNonNumerical() throws Exception {
+        injectSystemProperty("partition.vendor.verified", "foo");
+        setExecuteAdbCommandExpectations(
+                new CommandResult(CommandStatus.SUCCESS), "disable-verity");
+        setRebootExpectations();
+        setExecuteAdbCommandExpectations(new CommandResult(CommandStatus.SUCCESS), "remount");
+        EasyMock.expect(mMockStateMonitor.waitForDeviceAvailable()).andReturn(mMockIDevice);
+        replayMocks();
+        mTestDevice.remountVendorWritable();
+        verifyMocks();
+    }
+
+    /**
      * Test that {@link TestDevice#getBuildSigningKeys()} works for the typical "test-keys" case
      * @throws Exception
      */
@@ -3473,7 +3538,7 @@
                         "feature:com.google.android.feature.GOOGLE_EXPERIENCE";
             }
         };
-        assertTrue(mTestDevice.hasFeature("com.google.android.feature.EXCHANGE_6_2"));
+        assertTrue(mTestDevice.hasFeature("feature:com.google.android.feature.EXCHANGE_6_2"));
     }
 
     /**
@@ -3492,6 +3557,22 @@
     }
 
     /**
+     * Unit test for {@link TestDevice#hasFeature(String)} on partly matching case.
+     */
+    public void testHasFeature_partly_matching() throws Exception {
+        mTestDevice = new TestableTestDevice() {
+            @Override
+            public String executeShellCommand(String command) throws DeviceNotAvailableException {
+                return "feature:com.google.android.feature.EXCHANGE_6_2\n" +
+                        "feature:com.google.android.feature.GOOGLE_BUILD\n" +
+                        "feature:com.google.android.feature.GOOGLE_EXPERIENCE";
+            }
+        };
+        assertFalse(mTestDevice.hasFeature("feature:com.google.android.feature"));
+        assertTrue(mTestDevice.hasFeature("feature:com.google.android.feature.EXCHANGE_6_2"));
+    }
+
+    /**
      * Unit test for {@link TestDevice#getSetting(int, String, String)}.
      */
     public void testGetSetting() throws Exception {
@@ -3873,11 +3954,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) {
+            if (result.length != 107 && result.length != 117 && result.length != 139) {
                 fail(
                         String.format(
                                 "Should have compress the length as expected, got %s, "
-                                        + "expected 107 or 117",
+                                        + "expected 107 or 117 or 139",
                                 result.length));
             }
 
@@ -3885,7 +3966,13 @@
             Assert.assertEquals(3000, testImage.data.length);
             result = mTestDevice.compressRawImage(testImage, "JPEG", true);
             // Size after compressing as JPEG
-            Assert.assertEquals(1041, result.length);
+            if (result.length != 1041 && result.length != 851) {
+                fail(
+                        String.format(
+                                "Should have compress the length as expected, got %s, "
+                                        + "expected 851 or 1041",
+                                result.length));
+            }
         } finally {
             if (testImage != null) {
                 testImage.data = null;
diff --git a/tests/src/com/android/tradefed/device/metric/BaseDeviceMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/BaseDeviceMetricCollectorTest.java
index dd6bde5..70bc6b7 100644
--- a/tests/src/com/android/tradefed/device/metric/BaseDeviceMetricCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/BaseDeviceMetricCollectorTest.java
@@ -485,4 +485,49 @@
         assertTrue(allValues.get(2).containsKey("onteststart"));
         assertTrue(allValues.get(2).containsKey("ontestend"));
     }
+
+    /**
+     * Test that onTestEnd with TestDescription formal supercedes the method signature without a
+     * TestDescription.
+     */
+    @Test
+    public void testOnTestEndWithTestDescription() throws Exception {
+        mBase =
+                new TwoMetricsBaseCollector() {
+                    @Override
+                    public void onTestEnd(
+                            DeviceMetricData testData,
+                            final Map<String, Metric> currentTestCaseMetrics,
+                            TestDescription test) {
+                        testData.addMetric(
+                                test.getTestName(),
+                                Metric.newBuilder()
+                                        .setMeasurements(
+                                                Measurements.newBuilder()
+                                                        .setSingleString("value1")));
+                    }
+                };
+        mBase.init(mContext, mMockListener);
+        mBase.invocationStarted(mContext);
+        mBase.testRunStarted("testRun", 1);
+        TestDescription test = new TestDescription("class", "method");
+        mBase.testStarted(test);
+        mBase.testEnded(test, new HashMap<String, Metric>());
+        mBase.testRunEnded(0L, new HashMap<String, Metric>());
+        mBase.invocationEnded(0L);
+
+        Mockito.verify(mMockListener, times(1)).invocationStarted(Mockito.any());
+        Mockito.verify(mMockListener, times(1)).testRunStarted("testRun", 1);
+        Mockito.verify(mMockListener, times(1)).testStarted(Mockito.eq(test), Mockito.anyLong());
+        Mockito.verify(mMockListener, times(1))
+                .testEnded(Mockito.eq(test), Mockito.anyLong(), mCapturedMetrics.capture());
+
+        Mockito.verify(mMockListener, times(1))
+                .testRunEnded(Mockito.anyLong(), (HashMap<String, Metric>) Mockito.any());
+
+        List<HashMap<String, Metric>> allValues = mCapturedMetrics.getAllValues();
+        assertTrue(allValues.get(0).containsKey("onteststart"));
+        assertTrue(allValues.get(0).containsKey("method"));
+        assertTrue(!allValues.get(0).containsKey("ontestend"));
+    }
 }
diff --git a/tests/src/com/android/tradefed/result/ATestFileSystemLogSaverTest.java b/tests/src/com/android/tradefed/result/ATestFileSystemLogSaverTest.java
new file mode 100644
index 0000000..63de554
--- /dev/null
+++ b/tests/src/com/android/tradefed/result/ATestFileSystemLogSaverTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.result;
+
+import static org.junit.Assert.*;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+/** Unit tests for {@link ATestFileSystemLogSaver}. */
+@RunWith(JUnit4.class)
+public class ATestFileSystemLogSaverTest {
+
+    private File mReportDir;
+    private IBuildInfo mMockBuild;
+    private InvocationContext mContext;
+
+    @Before
+    public void setUp() throws Exception {
+        mReportDir = FileUtil.createTempDir("tmpdir");
+        mMockBuild = Mockito.mock(IBuildInfo.class);
+        mContext = new InvocationContext();
+        mContext.addDeviceBuildInfo("fakeDevice", mMockBuild);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        FileUtil.recursiveDelete(mReportDir);
+    }
+
+    /** Test if generated dir in specific path. */
+    @Test
+    public void testGenerateLogReportDir() throws ConfigurationException {
+        ATestFileSystemLogSaver saver = new ATestFileSystemLogSaver();
+        OptionSetter setter = new OptionSetter(saver);
+        setter.setOptionValue("atest-log-file-path", mReportDir.getAbsolutePath());
+        saver.invocationStarted(mContext);
+        File generatedDir = new File(saver.getLogReportDir().getPath());
+        File expectedParentFolder = generatedDir.getParentFile();
+        assertEquals(expectedParentFolder.getAbsolutePath(), mReportDir.getAbsolutePath());
+    }
+}
diff --git a/tests/src/com/android/tradefed/testtype/AndroidJUnitTestTest.java b/tests/src/com/android/tradefed/testtype/AndroidJUnitTestTest.java
index b041293..85b8245 100644
--- a/tests/src/com/android/tradefed/testtype/AndroidJUnitTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/AndroidJUnitTestTest.java
@@ -99,6 +99,7 @@
         };
         mAndroidJUnitTest.setRunnerName(AJUR);
         mAndroidJUnitTest.setPackageName(TEST_PACKAGE_VALUE);
+        mAndroidJUnitTest.setConfiguration(new Configuration("", ""));
         mAndroidJUnitTest.setDevice(mMockTestDevice);
         // default to no rerun, for simplicity
         mAndroidJUnitTest.setRerunMode(false);
diff --git a/tests/src/com/android/tradefed/testtype/GTestBaseTest.java b/tests/src/com/android/tradefed/testtype/GTestBaseTest.java
index 27a816f..9c0036f 100644
--- a/tests/src/com/android/tradefed/testtype/GTestBaseTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestBaseTest.java
@@ -15,7 +15,6 @@
  */
 package com.android.tradefed.testtype;
 
-import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -189,30 +188,4 @@
         gTestBase.setCollectTestsOnly(true);
         assertNull(gTestBase.split(5));
     }
-
-    /** Test that native coverage enabled will add the code coverage listener. */
-    @Test
-    public void testCoverage_addsCodeCoverageListener() throws ConfigurationException {
-        GTestBase gTestBase = new GTestBaseImpl();
-        mSetter = new OptionSetter(gTestBase);
-        mSetter.setOptionValue("coverage", "true");
-
-        ITestInvocationListener listener =
-                gTestBase.addNativeCoverageListenerIfEnabled(mMockTestDevice, mMockListener);
-
-        assertThat(listener).isInstanceOf(NativeCodeCoverageListener.class);
-    }
-
-    /** Test that when native coverage is disabled, the code coverage listener is not added. */
-    @Test
-    public void testNoCoverage_doesNotAddCodeCoverageListener() throws ConfigurationException {
-        GTestBase gTestBase = new GTestBaseImpl();
-        mSetter = new OptionSetter(gTestBase);
-        mSetter.setOptionValue("coverage", "false");
-
-        ITestInvocationListener listener =
-                gTestBase.addNativeCoverageListenerIfEnabled(mMockTestDevice, mMockListener);
-
-        assertThat(listener).isSameAs(mMockListener);
-    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/GTestTest.java b/tests/src/com/android/tradefed/testtype/GTestTest.java
index 2a5e10c..07f365d 100644
--- a/tests/src/com/android/tradefed/testtype/GTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestTest.java
@@ -22,12 +22,14 @@
 
 import com.android.ddmlib.FileListingService;
 import com.android.ddmlib.IShellOutputReceiver;
+import com.android.tradefed.config.Configuration;
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.CollectingOutputReceiver;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.MockFileUtil;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.testtype.coverage.CoverageOptions;
 
 import org.easymock.EasyMock;
 import org.junit.Before;
@@ -36,6 +38,8 @@
 import org.junit.runners.JUnit4;
 
 import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 
 
@@ -49,6 +53,10 @@
     private GTest mGTest;
     private OptionSetter mSetter;
 
+    private Configuration mConfiguration;
+    private CoverageOptions mCoverageOptions;
+    private OptionSetter mCoverageOptionsSetter;
+
     /** Helper to initialize the various EasyMocks we'll need. */
     @Before
     public void setUp() throws Exception {
@@ -79,6 +87,14 @@
                 };
         mGTest.setDevice(mMockITestDevice);
         mSetter = new OptionSetter(mGTest);
+
+        // Set up the coverage options
+        mConfiguration = new Configuration("", "");
+        mCoverageOptions = new CoverageOptions();
+        mCoverageOptionsSetter = new OptionSetter(mCoverageOptions);
+
+        mConfiguration.setCoverageOptions(mCoverageOptions);
+        mGTest.setConfiguration(mConfiguration);
     }
 
     /**
@@ -146,7 +162,6 @@
         mMockITestDevice.executeShellCommand(EasyMock.contains(test2),
                 EasyMock.same(mMockReceiver), EasyMock.anyLong(),
                 (TimeUnit)EasyMock.anyObject(), EasyMock.anyInt());
-
         replayMocks();
 
         mGTest.run(mMockInvocationListener);
@@ -414,6 +429,118 @@
         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.executeShellCommand("mkdir /data/misc/trace/testcoverage"))
+                .andReturn("");
+        EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
+        EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 -1")).andReturn("");
+        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(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.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("");
+
+        // 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(mMockInvocationListener);
+        verifyMocks();
+    }
+
     @Test
     public void testGetFileName() {
         String expected = "bar";
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java
index f00cab1..b44f2f9 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java
@@ -24,6 +24,7 @@
 import com.android.ddmlib.IDevice;
 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
 import com.android.ddmlib.testrunner.ITestRunListener;
+import com.android.tradefed.config.Configuration;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
@@ -83,6 +84,7 @@
                 return "runner";
             }
         };
+        mMockITest.setConfiguration(new Configuration("", ""));
         mMockITest.setDevice(mMockTestDevice);
         mMockITest.setPackageName(TEST_PACKAGE_VALUE);
         mMockITest = Mockito.spy(mMockITest);
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationSerialTestTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationSerialTestTest.java
index 8c4b60e..67a2bce 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationSerialTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationSerialTestTest.java
@@ -17,6 +17,7 @@
 
 import static org.junit.Assert.*;
 
+import com.android.tradefed.config.Configuration;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
@@ -72,6 +73,7 @@
                 };
 
         // mock out InstrumentationTest that will be used to create InstrumentationSerialTest
+        mockITest.setConfiguration(new Configuration("", ""));
         mockITest.setDevice(mMockTestDevice);
         mockITest.setPackageName(packageName);
 
@@ -118,6 +120,7 @@
                     }
                 };
         // mock out InstrumentationTest that will be used to create InstrumentationSerialTest
+        mockITest.setConfiguration(new Configuration("", ""));
         mockITest.setDevice(mMockTestDevice);
         mockITest.setPackageName(packageName);
         mInstrumentationSerialTest = new InstrumentationSerialTest(mockITest, testList) {
@@ -190,6 +193,7 @@
                 };
 
         // mock out InstrumentationTest that will be used to create InstrumentationSerialTest
+        mockITest.setConfiguration(new Configuration("", ""));
         mockITest.setDevice(mMockTestDevice);
         mockITest.setPackageName(packageName);
         // A test-package was specified for the original instrumentation.
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
index 157683f..fafc5c4 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
@@ -38,7 +38,9 @@
 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
 import com.android.ddmlib.testrunner.InstrumentationResultParser;
 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.tradefed.config.Configuration;
 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;
@@ -51,6 +53,7 @@
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.testtype.coverage.CoverageOptions;
 import com.android.tradefed.testtype.suite.GranularRetriableTestWrapperTest.CalledMetricCollector;
 import com.android.tradefed.util.ListInstrumentationParser;
 import com.android.tradefed.util.ListInstrumentationParser.InstrumentationTarget;
@@ -94,6 +97,11 @@
     /** The {@link InstrumentationTest} under test, with all dependencies mocked out */
     private InstrumentationTest mInstrumentationTest;
 
+    // The configuration objects.
+    private IConfiguration mConfig = null;
+    private CoverageOptions mCoverageOptions = null;
+    private OptionSetter mCoverageOptionsSetter = null;
+
     // The mock objects.
     @Mock IDevice mMockIDevice;
     @Mock ITestDevice mMockTestDevice;
@@ -120,7 +128,7 @@
     }
 
     @Before
-    public void setUp() {
+    public void setUp() throws ConfigurationException {
         MockitoAnnotations.initMocks(this);
 
         doReturn(mMockIDevice).when(mMockTestDevice).getIDevice();
@@ -139,6 +147,14 @@
         mInstrumentationTest.setDevice(mMockTestDevice);
         mInstrumentationTest.setListInstrumentationParser(mMockListInstrumentationParser);
         mInstrumentationTest.setReRunUsingTestFile(false);
+
+        // Set up configuration.
+        mConfig = new Configuration("", "");
+        mCoverageOptions = new CoverageOptions();
+        mCoverageOptionsSetter = new OptionSetter(mCoverageOptions);
+
+        mConfig.setCoverageOptions(mCoverageOptions);
+        mInstrumentationTest.setConfiguration(mConfig);
     }
 
     /** Test normal run scenario. */
@@ -433,7 +449,7 @@
     @Test
     public void testRun_rerunCoverage() throws ConfigurationException, DeviceNotAvailableException {
         mInstrumentationTest.setRerunMode(true);
-        mInstrumentationTest.setCoverage(true);
+        mCoverageOptionsSetter.setOptionValue("coverage", "true");
 
         Collection<TestDescription> expectedTests = ImmutableList.of(TEST1, TEST2);
 
@@ -509,10 +525,12 @@
 
     /** Verify that all tests are re-run when there is a failure during a coverage run. */
     @Test
-    public void testRun_mergedCoverage() throws DeviceNotAvailableException {
+    public void testRun_mergedCoverage()
+            throws ConfigurationException, DeviceNotAvailableException {
         mInstrumentationTest.setRerunMode(true);
-        mInstrumentationTest.setCoverage(true);
         mInstrumentationTest.setMergeCoverageMeasurements(true);
+        mCoverageOptionsSetter.setOptionValue("coverage", "true");
+        mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "JACOCO");
 
         // Mock collected tests
         RunInstrumentationTestsAnswer runTests =
@@ -919,8 +937,10 @@
     }
 
     @Test
-    public void testAddCoverageListener_enabled() {
-        mInstrumentationTest.setCoverage(true);
+    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);
@@ -931,8 +951,8 @@
     }
 
     @Test
-    public void testAddCoverageListener_disabled() {
-        mInstrumentationTest.setCoverage(false);
+    public void testAddCoverageListener_disabled() throws ConfigurationException {
+        mCoverageOptionsSetter.setOptionValue("coverage", "false");
 
         ITestInvocationListener listener =
                 mInstrumentationTest.addJavaCoverageListenerIfEnabled(mMockListener);
diff --git a/tests/src/com/android/tradefed/testtype/JavaCodeCoverageListenerTest.java b/tests/src/com/android/tradefed/testtype/JavaCodeCoverageListenerTest.java
index 68f7a3b..58aef9b 100644
--- a/tests/src/com/android/tradefed/testtype/JavaCodeCoverageListenerTest.java
+++ b/tests/src/com/android/tradefed/testtype/JavaCodeCoverageListenerTest.java
@@ -28,15 +28,20 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
 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.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.JavaCodeCoverageFlusher;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
 import com.google.common.base.VerifyException;
+import com.google.common.collect.ImmutableList;
 import com.google.protobuf.ByteString;
 
 import org.jacoco.core.tools.ExecFileLoader;
@@ -63,6 +68,7 @@
 import java.io.OutputStream;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /** Unit tests for {@link JavaCodeCoverageListener}. */
@@ -82,17 +88,25 @@
     @Rule public TemporaryFolder folder = new TemporaryFolder();
 
     @Mock ITestDevice mMockDevice;
+    @Mock JavaCodeCoverageFlusher mMockFlusher;
 
     @Spy LogFileReader mFakeListener = new LogFileReader();
 
     /** Object under test. */
     JavaCodeCoverageListener mCodeCoverageListener;
 
+    CoverageOptions mCoverageOptions = null;
+    OptionSetter mCoverageOptionsSetter = null;
+
     @Before
-    public void setUp() {
+    public void setUp() throws ConfigurationException {
         MockitoAnnotations.initMocks(this);
 
-        mCodeCoverageListener = new JavaCodeCoverageListener(mMockDevice, false, mFakeListener);
+        mCoverageOptions = new CoverageOptions();
+        mCoverageOptionsSetter = new OptionSetter(mCoverageOptions);
+
+        mCodeCoverageListener =
+                new JavaCodeCoverageListener(mMockDevice, mCoverageOptions, false, mFakeListener);
     }
 
     @Test
@@ -170,7 +184,8 @@
             measurement.writeTo(out);
         }
 
-        mCodeCoverageListener = new JavaCodeCoverageListener(mMockDevice, true, mFakeListener);
+        mCodeCoverageListener =
+                new JavaCodeCoverageListener(mMockDevice, mCoverageOptions, true, mFakeListener);
 
         Map<String, String> metric = new HashMap<>();
         metric.put("coverageFilePath", DEVICE_PATH);
@@ -207,6 +222,42 @@
                 .isEqualTo(partiallyCovered);
     }
 
+    @Test
+    public void testCoverageFlush_producesMultipleMeasurements() throws Exception {
+        List<String> coverageFileList =
+                ImmutableList.of(
+                        "/data/misc/trace/com.android.test1.ec",
+                        "/data/misc/trace/com.android.test2.ec",
+                        "/data/misc/trace/com.google.test3.ec");
+
+        mCoverageOptionsSetter.setOptionValue("coverage-flush", "true");
+
+        // Setup mocks.
+        File coverageFile = folder.newFile("coverage.ec");
+        try (OutputStream out = new FileOutputStream(coverageFile)) {
+            COVERAGE_MEASUREMENT.writeTo(out);
+        }
+        doReturn(coverageFile).when(mMockDevice).pullFile(DEVICE_PATH);
+
+        for (String additionalFile : coverageFileList) {
+            File coverage = folder.newFile();
+            try (OutputStream out = new FileOutputStream(coverage)) {
+                COVERAGE_MEASUREMENT.writeTo(out);
+            }
+            doReturn(coverage).when(mMockDevice).pullFile(additionalFile);
+        }
+
+        doReturn(coverageFileList).when(mMockFlusher).forceCoverageFlush();
+
+        mCodeCoverageListener.setCoverageFlusher(mMockFlusher);
+
+        // Simulate a test run.
+        mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
+        Map<String, String> metric = new HashMap<>();
+        metric.put("coverageFilePath", DEVICE_PATH);
+        mCodeCoverageListener.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
+    }
+
     private static <T> String vmName(Class<T> clazz) {
         return clazz.getName().replace('.', '/');
     }
diff --git a/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java b/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java
index 98d88eb..a0c7f18 100644
--- a/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java
+++ b/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java
@@ -19,21 +19,27 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
 
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 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.android.tradefed.util.TarUtil;
 
 import com.google.common.base.VerifyException;
+import com.google.common.collect.ImmutableMap;
 import com.google.protobuf.ByteString;
 
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -49,7 +55,6 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URI;
-import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
@@ -58,7 +63,6 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.StringJoiner;
 import java.util.zip.ZipFile;
 
 /** Unit tests for {@link NativeCodeCoverageListener}. */
@@ -69,48 +73,40 @@
     private static final int TEST_COUNT = 5;
     private static final long ELAPSED_TIME = 1000;
 
-    private static final ByteString COVERAGE_MEASUREMENT =
-            ByteString.copyFromUtf8("Mi estas kovrado mezurado");
-
     @Rule public TemporaryFolder folder = new TemporaryFolder();
 
     @Mock ITestDevice mMockDevice;
 
     LogFileReader mFakeListener = new LogFileReader();
 
+    /** Options for coverage. */
+    CoverageOptions mCoverageOptions = null;
+
+    OptionSetter mCoverageOptionsSetter = null;
+
     /** Object under test. */
     NativeCodeCoverageListener mCodeCoverageListener;
 
     @Before
-    public void setUp() {
+    public void setUp() throws ConfigurationException {
         MockitoAnnotations.initMocks(this);
-
         mCodeCoverageListener = new NativeCodeCoverageListener(mMockDevice, mFakeListener);
+        mCoverageOptions = new CoverageOptions();
+        mCoverageOptionsSetter = new OptionSetter(mCoverageOptions);
     }
 
     @Test
     public void test_logsCoverageZip() throws DeviceNotAvailableException, IOException {
         // Setup mocks to write the coverage measurement to the file.
         doReturn(true).when(mMockDevice).enableAdbRoot();
-        doReturn(
-                        new StringJoiner("\n")
-                                .add("/data/misc/trace/proc/self/cwd/out/path/to/coverage.gcda")
-                                .add(
-                                        "/data/misc/trace/proc/self/cwd/out/path/to/.hidden/coverage2.gcda")
-                                .toString())
-                .when(mMockDevice)
-                .executeShellCommand(anyString());
-        doAnswer(
-                        inv -> {
-                            File destFile = (File) inv.getArgument(1);
-                            try (OutputStream out = new FileOutputStream(destFile)) {
-                                // Write the filename as the contents.
-                                out.write(destFile.getName().getBytes(StandardCharsets.UTF_8));
-                            }
-                            return true;
-                        })
-                .when(mMockDevice)
-                .pullFile(anyString(), any());
+        File tarGz =
+                createTarGz(
+                        ImmutableMap.of(
+                                "path/to/coverage.gcda",
+                                ByteString.copyFromUtf8("coverage.gcda"),
+                                "path/to/.hidden/coverage2.gcda",
+                                ByteString.copyFromUtf8("coverage2.gcda")));
+        doReturn(tarGz).when(mMockDevice).pullFile(anyString());
 
         // Simulate a test run.
         mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
@@ -140,7 +136,7 @@
     @Test
     public void testNoCoverageFiles_logsEmptyZip() throws DeviceNotAvailableException, IOException {
         doReturn(true).when(mMockDevice).enableAdbRoot();
-        doReturn("").when(mMockDevice).executeShellCommand(anyString());
+        doReturn(createTarGz(ImmutableMap.of())).when(mMockDevice).pullFile(anyString());
 
         // Simulate a test run.
         mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
@@ -160,14 +156,55 @@
     }
 
     @Test
+    public void testCoverageFlushAllProcesses_flushAllCommandCalled()
+            throws ConfigurationException, DeviceNotAvailableException, IOException {
+        mCoverageOptionsSetter.setOptionValue("coverage-flush", "true");
+
+        mCodeCoverageListener =
+                new NativeCodeCoverageListener(mMockDevice, mCoverageOptions, mFakeListener);
+
+        doReturn(true).when(mMockDevice).enableAdbRoot();
+        doReturn(true).when(mMockDevice).isAdbRoot();
+        doReturn(createTarGz(ImmutableMap.of())).when(mMockDevice).pullFile(anyString());
+
+        // Simulate a test run.
+        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");
+    }
+
+    @Test
+    public void testCoverageFlushSpecificProcesses_flushCommandCalled()
+            throws ConfigurationException, DeviceNotAvailableException, IOException {
+        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(createTarGz(ImmutableMap.of())).when(mMockDevice).pullFile(anyString());
+
+        // Simulate a test run.
+        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");
+    }
+
+    @Test
     public void testFailure_unableToPullFile() throws DeviceNotAvailableException {
         // Setup mocks.
         doReturn(true).when(mMockDevice).enableAdbRoot();
-        doReturn("/data/misc/trace/proc/self/cwd/out/some/path/to/coverage.gcda\n")
-                .when(mMockDevice)
-                .executeShellCommand(anyString());
-        doReturn(false).when(mMockDevice).pullFile(anyString(), any());
-
         // Simulate a test run.
         mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
 
@@ -184,6 +221,46 @@
         assertThat(mFakeListener.getLogs()).isEmpty();
     }
 
+    @Test
+    public void testNoCollectOnTestEnd_noCoverageMeasurements() throws Exception {
+        mCodeCoverageListener = new NativeCodeCoverageListener(mMockDevice, mFakeListener);
+        mCodeCoverageListener.setCollectOnTestEnd(false);
+
+        // Simute a test run.
+        mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
+        Map<String, String> metric = new HashMap<>();
+        mCodeCoverageListener.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
+
+        // Verify nothing was logged.
+        assertThat(mFakeListener.getLogs()).isEmpty();
+
+        // Setup mocks to write the coverage measurement to the file.
+        doReturn(true).when(mMockDevice).enableAdbRoot();
+        File tarGz =
+                createTarGz(
+                        ImmutableMap.of(
+                                "path/to/coverage.gcda", ByteString.copyFromUtf8("coverage.gcda")));
+        doReturn(tarGz).when(mMockDevice).pullFile(anyString());
+
+        // Manually call logCoverageMeasurements().
+        mCodeCoverageListener.logCoverageMeasurements("manual");
+
+        // Verify testLog(..) was called with the coverage file in a zip.
+        List<ByteString> logs = mFakeListener.getLogs();
+        assertThat(logs).hasSize(1);
+        File outputZip = folder.newFile("coverage.zip");
+        try (OutputStream out = new FileOutputStream(outputZip)) {
+            logs.get(0).writeTo(out);
+        }
+
+        URI uri = URI.create(String.format("jar:file:%s", outputZip));
+        try (FileSystem filesystem = FileSystems.newFileSystem(uri, new HashMap<>())) {
+            Path path = filesystem.getPath("/path/to/coverage.gcda");
+            assertThat(ByteString.readFrom(Files.newInputStream(path)))
+                    .isEqualTo(ByteString.copyFromUtf8("coverage.gcda"));
+        }
+    }
+
     /** An {@link ITestInvocationListener} which reads test log data streams for verification. */
     private static class LogFileReader implements ITestInvocationListener {
         private List<ByteString> mLogs = new ArrayList<>();
@@ -202,4 +279,21 @@
             return new ArrayList<>(mLogs);
         }
     }
+
+    /** Utility method to create .tar.gz files. */
+    private File createTarGz(Map<String, ByteString> fileContents) throws IOException {
+        File tarFile = folder.newFile("coverage.tar");
+        try (TarArchiveOutputStream out =
+                new TarArchiveOutputStream(new FileOutputStream(tarFile))) {
+            for (Map.Entry<String, ByteString> file : fileContents.entrySet()) {
+                TarArchiveEntry entry = new TarArchiveEntry(file.getKey());
+                entry.setSize(file.getValue().size());
+
+                out.putArchiveEntry(entry);
+                file.getValue().writeTo(out);
+                out.closeArchiveEntry();
+            }
+        }
+        return TarUtil.gzip(tarFile);
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/suite/AtestRunnerTest.java b/tests/src/com/android/tradefed/testtype/suite/AtestRunnerTest.java
index 9b74784..45f5a4d 100644
--- a/tests/src/com/android/tradefed/testtype/suite/AtestRunnerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/AtestRunnerTest.java
@@ -34,12 +34,14 @@
 import com.android.tradefed.testtype.InstrumentationTest;
 import com.android.tradefed.testtype.UiAutomatorTest;
 import com.android.tradefed.util.AbiUtils;
+import com.android.tradefed.util.FileUtil;
 
 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.IOException;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -50,6 +52,12 @@
 @RunWith(JUnit4.class)
 public class AtestRunnerTest {
 
+    private static final String TEST_CONFIG =
+        "<configuration description=\"Runs a stub tests part of some suite\">\n"
+            + "    <test class=\"com.android.tradefed.testtype.suite.SuiteModuleLoaderTest"
+            + "$TestInject\" />\n"
+            + "</configuration>";
+
     private static final String ABI = "armeabi-v7a";
     private static final String TEST_NAME_FMT = ABI + " %s";
     private static final String INSTRUMENTATION_TEST_NAME =
@@ -123,6 +131,55 @@
     }
 
     @Test
+    public void testLoadTests_WithTFConfigSpecified() throws Exception {
+        setter = new OptionSetter(mSpyRunner);
+        setter.setOptionValue("suite-config-prefix", "suite");
+        setter.setOptionValue("tf-config-path", "suite/base-suite1");
+        LinkedHashMap<String, IConfiguration> configMap = mSpyRunner.loadTests();
+        assertEquals(1, configMap.size());
+        String testName = String.format(TEST_NAME_FMT, "suite/base-suite1");
+        assertTrue(configMap.containsKey(testName));
+    }
+
+    @Test
+    public void testLoadTests_WithModuleAndTFConfigSpecified() throws Exception {
+        File tmpDir = FileUtil.createTempDir("some-dir");
+        String filePath = createModuleConfig(tmpDir, "TestModule");
+        try {
+            mSpyRunner.setupFilters(tmpDir);
+            setter = new OptionSetter(mSpyRunner);
+            setter.setOptionValue("suite-config-prefix", "suite");
+            setter.setOptionValue("tf-config-path", "suite/base-suite1");
+            setter.setOptionValue("module-config-path", filePath);
+            LinkedHashMap<String, IConfiguration> configMap = mSpyRunner.loadTests();
+            assertEquals(2, configMap.size());
+            String testName = String.format(TEST_NAME_FMT, "TestModule");
+            assertTrue(configMap.containsKey(testName));
+            testName = String.format(TEST_NAME_FMT, "suite/base-suite1");
+            assertTrue(configMap.containsKey(testName));
+        } finally {
+            FileUtil.recursiveDelete(tmpDir);
+        }
+    }
+
+    @Test
+    public void testLoadTests_WithModuleConfigSpecified() throws Exception {
+        File tmpDir = FileUtil.createTempDir("some-dir");
+        String filePath = createModuleConfig(tmpDir, "TestModule");
+        try {
+            mSpyRunner.setupFilters(tmpDir);
+            setter = new OptionSetter(mSpyRunner);
+            setter.setOptionValue("module-config-path", filePath);
+            LinkedHashMap<String, IConfiguration> configMap = mSpyRunner.loadTests();
+            assertEquals(1, configMap.size());
+            String testName = String.format(TEST_NAME_FMT, "TestModule");
+            assertTrue(configMap.containsKey(testName));
+        } finally {
+            FileUtil.recursiveDelete(tmpDir);
+        }
+    }
+
+    @Test
     public void testLoadTests_ignoreFilter() throws Exception {
         setter = new OptionSetter(mSpyRunner);
         setter.setOptionValue("suite-config-prefix", "suite");
@@ -212,4 +269,10 @@
         List<ITestInvocationListener> listeners = mSpyRunner.createModuleListeners();
         assertEquals(1, listeners.size());
     }
+
+    private String createModuleConfig(File dir, String moduleName) throws IOException {
+        File moduleConfig = new File(dir, moduleName + SuiteModuleLoader.CONFIG_EXT);
+        FileUtil.writeToFile(TEST_CONFIG, moduleConfig);
+        return moduleConfig.getAbsolutePath();
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java b/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
index df7bfd2..fb7647f 100644
--- a/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
@@ -133,6 +133,41 @@
         }
     }
 
+    /**
+     * Test that we create a module and the parameterized version of it for the include filter if
+     * not explicitly excluded.
+     */
+    @Test
+    public void testSetupFilters_parameterized_filter() throws Exception {
+        File tmpDir = FileUtil.createTempDir(TEST_MODULE);
+        File moduleConfig = new File(tmpDir, "CtsGestureTestCases.config");
+        moduleConfig.createNewFile();
+        try {
+            OptionSetter setter = new OptionSetter(mRunner);
+            setter.setOptionValue("enable-parameterized-modules", "true");
+            // The Gesture module has a parameter "instant".
+            setter.setOptionValue("module", "Gesture");
+            mRunner.setupFilters(tmpDir);
+            assertEquals(2, mRunner.getIncludeFilter().size());
+            assertThat(
+                    mRunner.getIncludeFilter(),
+                    hasItem(
+                            new SuiteTestFilter(
+                                            mRunner.getRequestedAbi(), "CtsGestureTestCases", null)
+                                    .toString()));
+            assertThat(
+                    mRunner.getIncludeFilter(),
+                    hasItem(
+                            new SuiteTestFilter(
+                                            mRunner.getRequestedAbi(),
+                                            "CtsGestureTestCases[instant]",
+                                            null)
+                                    .toString()));
+        } finally {
+            FileUtil.recursiveDelete(tmpDir);
+        }
+    }
+
     @Test
     public void testSetupFilters_match() throws Exception {
         File tmpDir = FileUtil.createTempDir(TEST_MODULE);
diff --git a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
index 8374a80..0fb2bb1 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
@@ -18,6 +18,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -29,6 +30,7 @@
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.ConfigurationFactory;
 import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.IConfigurationReceiver;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -170,7 +172,10 @@
         }
     }
 
-    public static class StubCollectingTest implements IRemoteTest, ITestFilterReceiver {
+    public static class StubCollectingTest
+            implements IRemoteTest, IConfigurationReceiver, ITestFilterReceiver {
+        private IConfiguration mConfiguration;
+
         private DeviceNotAvailableException mException;
         private RuntimeException mRunException;
         private String mFailed;
@@ -189,6 +194,10 @@
             mFailed = errMessage;
         }
 
+        public IConfiguration getConfiguration() {
+            return mConfiguration;
+        }
+
         @Override
         public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
             listener.testRunStarted(TEST_CONFIG_NAME, 1);
@@ -211,6 +220,11 @@
         }
 
         @Override
+        public void setConfiguration(IConfiguration config) {
+            mConfiguration = config;
+        }
+
+        @Override
         public void addIncludeFilter(String filter) {
             // ignored
         }
@@ -1442,6 +1456,64 @@
         EasyMock.verify(moduleListener);
     }
 
+    /**
+     * Test that {@link CoverageOptions} are passed from the main configuration to the module
+     * configuration.
+     */
+    @Test
+    public void testRun_coverageOptionsCopied() throws Exception {
+        ITestInvocationListener moduleListener = EasyMock.createMock(ITestInvocationListener.class);
+        StubCollectingTest test = new StubCollectingTest();
+        mTestSuite =
+                new TestSuiteImpl() {
+                    @Override
+                    public LinkedHashMap<String, IConfiguration> loadTests() {
+                        LinkedHashMap<String, IConfiguration> testConfig = new LinkedHashMap<>();
+                        try {
+                            IConfiguration fake =
+                                    ConfigurationFactory.getInstance()
+                                            .createConfigurationFromArgs(
+                                                    new String[] {EMPTY_CONFIG});
+                            fake.setTest(test);
+                            testConfig.put(TEST_CONFIG_NAME, fake);
+                        } catch (ConfigurationException e) {
+                            CLog.e(e);
+                            throw new RuntimeException(e);
+                        }
+                        return testConfig;
+                    }
+                };
+        mTestSuite.setDevice(mMockDevice);
+        mTestSuite.setBuild(mMockBuildInfo);
+        mTestSuite.setConfiguration(mStubMainConfiguration);
+        mTestSuite.setInvocationContext(mContext);
+
+        List<ISystemStatusChecker> sysChecker = new ArrayList<ISystemStatusChecker>();
+        sysChecker.add(mMockSysChecker);
+        mTestSuite.setSystemStatusChecker(sysChecker);
+        EasyMock.expect(mMockSysChecker.preExecutionCheck(EasyMock.eq(mMockDevice)))
+                .andReturn(new StatusCheckerResult(CheckStatus.SUCCESS));
+        EasyMock.expect(mMockSysChecker.postExecutionCheck(EasyMock.eq(mMockDevice)))
+                .andReturn(new StatusCheckerResult(CheckStatus.SUCCESS));
+        mMockListener.testModuleStarted(EasyMock.anyObject());
+        mMockListener.testRunStarted(
+                EasyMock.eq(TEST_CONFIG_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
+        TestDescription testDescription = new TestDescription(EMPTY_CONFIG, EMPTY_CONFIG);
+        mMockListener.testStarted(testDescription, 0);
+        mMockListener.testEnded(testDescription, 5, new HashMap<String, Metric>());
+        mMockListener.testRunEnded(
+                EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
+        mMockListener.testModuleEnded();
+        replayMocks();
+        mTestSuite.run(mMockListener);
+        verifyMocks();
+
+        // Check that CoverageOptions was copied to the module.
+        assertSame(
+                mStubMainConfiguration.getCoverageOptions(),
+                test.getConfiguration().getCoverageOptions());
+    }
+
     /** Test for {@link ITestSuite#run(ITestInvocationListener)} when a module listener is used. */
     @Test
     public void testRun_GranularRerunwithModuleListener() throws Exception {
diff --git a/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java b/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
index 4c7a8ba..4eabd26 100644
--- a/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
@@ -17,6 +17,7 @@
 
 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 com.android.tradefed.config.IConfiguration;
@@ -56,6 +57,12 @@
                     + "$TestInject\" />\n"
                     + "</configuration>";
 
+    private static final String TEST_INSTANT_CONFIG =
+            "<configuration description=\"Runs a stub tests part of some suite\">\n"
+                    + "    <option name=\"config-descriptor:metadata\" key=\"parameter\" value=\"instant_app\" />"
+                    + "    <test class=\"com.android.tradefed.testtype.suite.TestSuiteStub\" />\n"
+                    + "</configuration>";
+
     private SuiteModuleLoader mRepo;
     private File mTestsDir;
     private Set<IAbi> mAbis;
@@ -83,6 +90,11 @@
         FileUtil.writeToFile(TEST_CONFIG, module);
     }
 
+    private void createInstantModuleConfig(String moduleName) throws IOException {
+        File module = new File(mTestsDir, moduleName + SuiteModuleLoader.CONFIG_EXT);
+        FileUtil.writeToFile(TEST_INSTANT_CONFIG, module);
+    }
+
     @OptionClass(alias = "test-inject")
     public static class TestInject implements IRemoteTest {
         @Option(name = "simple-string")
@@ -215,4 +227,123 @@
         TestInject checker = (TestInject) config.getTests().get(0);
         assertEquals("value1", checker.testAlias);
     }
+
+    /**
+     * Test that if the base module is excluded in full, the filters of parameterized modules are
+     * still populated with the proper filters.
+     */
+    @Test
+    public void testFilterParameterized() throws Exception {
+        Map<String, List<SuiteTestFilter>> excludeFilters = new LinkedHashMap<>();
+        createInstantModuleConfig("basemodule");
+        SuiteTestFilter fullFilter = SuiteTestFilter.createFrom("armeabi-v7a basemodule");
+        excludeFilters.put("armeabi-v7a basemodule", Arrays.asList(fullFilter));
+
+        SuiteTestFilter instantMethodFilter =
+                SuiteTestFilter.createFrom(
+                        "armeabi-v7a basemodule[instant] NativeDnsAsyncTest#Async_Cancel");
+        excludeFilters.put("armeabi-v7a basemodule[instant]", Arrays.asList(instantMethodFilter));
+
+        mRepo =
+                new SuiteModuleLoader(
+                        new LinkedHashMap<String, List<SuiteTestFilter>>(),
+                        excludeFilters,
+                        new ArrayList<>(),
+                        new ArrayList<>());
+        mRepo.setParameterizedModules(true);
+
+        List<String> patterns = new ArrayList<>();
+        patterns.add(".*.config");
+        patterns.add(".*.xml");
+        LinkedHashMap<String, IConfiguration> res =
+                mRepo.loadConfigsFromDirectory(
+                        Arrays.asList(mTestsDir), mAbis, null, null, patterns);
+        assertEquals(1, res.size());
+        // Full module was excluded completely
+        IConfiguration instantModule = res.get("armeabi-v7a basemodule[instant]");
+        assertNotNull(instantModule);
+        TestSuiteStub stubTest = (TestSuiteStub) instantModule.getTests().get(0);
+        assertEquals(1, stubTest.getExcludeFilters().size());
+        assertEquals(
+                "NativeDnsAsyncTest#Async_Cancel", stubTest.getExcludeFilters().iterator().next());
+    }
+
+    /**
+     * Test that the configuration can be found if specifying specific path.
+     */
+    @Test
+    public void testLoadConfigsFromSpecifiedPaths_OneModule() throws Exception {
+        createModuleConfig("module1");
+        File module1 = new File(mTestsDir, "module1" + SuiteModuleLoader.CONFIG_EXT);
+
+        mRepo =
+                new SuiteModuleLoader(
+                        new LinkedHashMap<String, List<SuiteTestFilter>>(),
+                        new LinkedHashMap<String, List<SuiteTestFilter>>(),
+                        new ArrayList<>(),
+                        new ArrayList<>());
+
+        LinkedHashMap<String, IConfiguration> res =
+            mRepo.loadConfigsFromSpecifiedPaths(
+                Arrays.asList(module1), mAbis, null);
+        assertEquals(1, res.size());
+        assertNotNull(res.get("armeabi-v7a module1"));
+    }
+
+    /**
+     * Test that multiple configurations can be found if specifying specific paths.
+     */
+    @Test
+    public void testLoadConfigsFromSpecifiedPaths_MultipleModules() throws Exception {
+        createModuleConfig("module1");
+        File module1 = new File(mTestsDir, "module1" + SuiteModuleLoader.CONFIG_EXT);
+        createModuleConfig("module2");
+        File module2 = new File(mTestsDir, "module2" + SuiteModuleLoader.CONFIG_EXT);
+
+        mRepo =
+                new SuiteModuleLoader(
+                        new LinkedHashMap<String, List<SuiteTestFilter>>(),
+                        new LinkedHashMap<String, List<SuiteTestFilter>>(),
+                        new ArrayList<>(),
+                        new ArrayList<>());
+
+        LinkedHashMap<String, IConfiguration> res =
+            mRepo.loadConfigsFromSpecifiedPaths(
+                Arrays.asList(module1, module2), mAbis, null);
+        assertEquals(2, res.size());
+        assertNotNull(res.get("armeabi-v7a module1"));
+        assertNotNull(res.get("armeabi-v7a module2"));
+    }
+
+    /**
+     * Test that configuration can be found correctly if specifying specific paths but someone is
+     * excluded.
+     */
+    @Test
+    public void testLoadConfigsFromSpecifiedPaths_WithExcludeFilter() throws Exception {
+        createModuleConfig("module1");
+        File module1 = new File(mTestsDir, "module1" + SuiteModuleLoader.CONFIG_EXT);
+        createModuleConfig("module2");
+        File module2 = new File(mTestsDir, "module2" + SuiteModuleLoader.CONFIG_EXT);
+
+        Map<String, List<SuiteTestFilter>> excludeFilters = new LinkedHashMap<>();
+        SuiteTestFilter filter =
+            SuiteTestFilter.createFrom(
+                "armeabi-v7a module2");
+        excludeFilters.put("armeabi-v7a module2", Arrays.asList(filter));
+
+        mRepo =
+            new SuiteModuleLoader(
+                new LinkedHashMap<String, List<SuiteTestFilter>>(),
+                excludeFilters,
+                new ArrayList<>(),
+                new ArrayList<>());
+
+        LinkedHashMap<String, IConfiguration> res =
+            mRepo.loadConfigsFromSpecifiedPaths(
+                Arrays.asList(module1, module2), mAbis, null);
+        assertEquals(1, res.size());
+        assertNotNull(res.get("armeabi-v7a module1"));
+        assertNull(res.get("armeabi-v7a module2"));
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java b/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
index bbf22c4..03f39c2 100644
--- a/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
@@ -20,6 +20,8 @@
 
 import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
 import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.ConfigurationFactory;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -27,12 +29,15 @@
 import com.android.tradefed.testtype.Abi;
 import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.IRemoteTest;
-import com.android.tradefed.testtype.InstrumentationTest;
+import com.android.tradefed.testtype.StubTest;
 import com.android.tradefed.util.AbiUtils;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.ZipUtil;
+import com.android.tradefed.util.testmapping.TestInfo;
 import com.android.tradefed.util.testmapping.TestMapping;
 
+import com.android.tradefed.util.testmapping.TestOption;
+import java.util.ArrayList;
 import org.easymock.EasyMock;
 import org.junit.Before;
 import org.junit.Test;
@@ -57,13 +62,17 @@
 
     private static final String ABI_1 = "arm64-v8a";
     private static final String ABI_2 = "armeabi-v7a";
+    private static final String DISABLED_PRESUBMIT_TESTS = "disabled-presubmit-tests";
+    private static final String EMPTY_CONFIG = "empty";
     private static final String NON_EXISTING_DIR = "non-existing-dir";
+    private static final String TEST_CONFIG_NAME = "test";
     private static final String TEST_DATA_DIR = "testdata";
     private static final String TEST_MAPPING = "TEST_MAPPING";
     private static final String TEST_MAPPINGS_ZIP = "test_mappings.zip";
-    private static final String DISABLED_PRESUBMIT_TESTS = "disabled-presubmit-tests";
 
     private TestMappingSuiteRunner mRunner;
+    private OptionSetter mOptionSetter;
+    private TestMappingSuiteRunner mRunner2;
     private IDeviceBuildInfo mBuildInfo;
     private ITestDevice mMockDevice;
 
@@ -75,8 +84,16 @@
         mRunner.setBuild(mBuildInfo);
         mRunner.setDevice(mMockDevice);
 
-        EasyMock.expect(mBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR)).andReturn(null);
-        EasyMock.expect(mBuildInfo.getTestsDir()).andReturn(new File(NON_EXISTING_DIR));
+        mOptionSetter = new OptionSetter(mRunner);
+        mOptionSetter.setOptionValue("suite-config-prefix", "suite");
+
+        mRunner2 = new FakeTestMappingSuiteRunner();
+        mRunner2.setBuild(mBuildInfo);
+        mRunner2.setDevice(mMockDevice);
+
+        EasyMock.expect(mBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR)).andReturn(null)
+                .anyTimes();
+        EasyMock.expect(mBuildInfo.getTestsDir()).andReturn(new File(NON_EXISTING_DIR)).anyTimes();
         EasyMock.expect(mMockDevice.getProperty(EasyMock.anyObject())).andReturn(ABI_1);
         EasyMock.expect(mMockDevice.getProperty(EasyMock.anyObject())).andReturn(ABI_2);
         EasyMock.replay(mBuildInfo, mMockDevice);
@@ -94,6 +111,43 @@
             abis.add(new Abi(ABI_2, AbiUtils.getBitness(ABI_2)));
             return abis;
         }
+
+        @Override
+        List<IRemoteTest> createIndividualTests(Set<TestInfo> testInfos, String configPath) {
+            IRemoteTest fakeTest = EasyMock.createMock(IRemoteTest.class);
+            return new ArrayList<>(Arrays.asList(fakeTest));
+        }
+    }
+
+    /**
+     * Test TestMappingSuiteRunner that create a fake IConfiguration with fake a test object.
+     */
+    public static class FakeTestMappingSuiteRunner extends TestMappingSuiteRunner {
+        @Override
+        public Set<IAbi> getAbis(ITestDevice device) throws DeviceNotAvailableException {
+            Set<IAbi> abis = new HashSet<>();
+            abis.add(new Abi(ABI_1, AbiUtils.getBitness(ABI_1)));
+            abis.add(new Abi(ABI_2, AbiUtils.getBitness(ABI_2)));
+            return abis;
+        }
+
+        @Override
+        public LinkedHashMap<String, IConfiguration> loadingStrategy(Set<IAbi> abis,
+            List<File> testsDirs, String suitePrefix, String suiteTag) {
+            LinkedHashMap<String, IConfiguration> testConfig = new LinkedHashMap<>();
+            try {
+                IConfiguration config =
+                        ConfigurationFactory.getInstance()
+                                .createConfigurationFromArgs(new String[] {EMPTY_CONFIG});
+                config.setTest(new StubTest());
+                config.getConfigurationDescription().setModuleName(TEST_CONFIG_NAME);
+                testConfig.put(TEST_CONFIG_NAME, config);
+
+            } catch (ConfigurationException e) {
+                throw new RuntimeException(e);
+            }
+            return testConfig;
+        }
     }
 
     /**
@@ -108,6 +162,17 @@
         mRunner.loadTests();
     }
 
+    /**
+     * Test for {@link TestMappingSuiteRunner#loadTests()} to fail when both options include-filter
+     * and test-mapping-path are set.
+     */
+    @Test(expected = RuntimeException.class)
+    public void testLoadTests_conflictOptions() throws Exception {
+        mOptionSetter.setOptionValue("include-filter", "test1");
+        mOptionSetter.setOptionValue("test-mapping-path", "path1");
+        mRunner.loadTests();
+    }
+
     /** Test for {@link TestMappingSuiteRunner#loadTests()} to fail when no test option is set. */
     @Test(expected = RuntimeException.class)
     public void testLoadTests_noOption() throws Exception {
@@ -173,14 +238,7 @@
             assertTrue(mRunner.getIncludeFilter().contains("test2"));
             assertTrue(mRunner.getIncludeFilter().contains("instrument"));
             assertTrue(mRunner.getIncludeFilter().contains("suite/stub1"));
-            // Filters are applied directly
-            assertTrue(mRunner.getExcludeFilter().contains("suite/stub1 filter.com"));
-            assertTrue(mRunner.getIncludeFilter().contains("suite/stub2 filter.com"));
-
-            // Check module-arg work as expected.
-            InstrumentationTest test =
-                    (InstrumentationTest) configMap.get("arm64-v8a instrument").getTests().get(0);
-            assertEquals("some-name", test.getRunName());
+            assertTrue(mRunner.getIncludeFilter().contains("suite/stub2"));
 
             assertEquals(6, configMap.size());
             assertTrue(configMap.containsKey(ABI_1 + " instrument"));
@@ -478,4 +536,248 @@
         assertTrue(configMap.containsKey(ABI_2 + " suite/stubAbi"));
         EasyMock.verify(mockDevice);
     }
+
+    /**
+     * Test for {@link TestMappingSuiteRunner#loadTests()} that when force-test-mapping-module is
+     * specified, tests would be filtered.
+     */
+    @Test
+    public void testLoadTestsWithModule() throws Exception {
+        File tempDir = null;
+        try {
+            OptionSetter setter = new OptionSetter(mRunner);
+            setter.setOptionValue("test-mapping-test-group", "postsubmit");
+            setter.setOptionValue("force-test-mapping-module", "suite/stub1");
+
+            tempDir = FileUtil.createTempDir("test_mapping");
+
+            File srcDir = FileUtil.createTempDir("src", tempDir);
+            String srcFile =
+                    File.separator + TEST_DATA_DIR + File.separator + DISABLED_PRESUBMIT_TESTS;
+            InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, srcDir, DISABLED_PRESUBMIT_TESTS);
+
+            srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_1";
+            resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, srcDir, TEST_MAPPING);
+            File subDir = FileUtil.createTempDir("sub_dir", srcDir);
+            srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_2";
+            resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, subDir, TEST_MAPPING);
+
+            List<File> filesToZip =
+                    Arrays.asList(srcDir, new File(tempDir, DISABLED_PRESUBMIT_TESTS));
+            File zipFile = Paths.get(tempDir.getAbsolutePath(), TEST_MAPPINGS_ZIP).toFile();
+            ZipUtil.createZip(filesToZip, zipFile);
+
+            IDeviceBuildInfo mockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
+            EasyMock.expect(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR))
+                    .andReturn(null);
+            EasyMock.expect(mockBuildInfo.getTestsDir()).andReturn(new File("non-existing-dir"));
+            EasyMock.expect(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).andReturn(zipFile);
+
+            mRunner.setBuild(mockBuildInfo);
+            EasyMock.replay(mockBuildInfo);
+
+            LinkedHashMap<String, IConfiguration> configMap = mRunner.loadTests();
+            assertEquals(2, configMap.size());
+            assertTrue(configMap.containsKey(ABI_1 + " suite/stub1"));
+            assertTrue(configMap.containsKey(ABI_2 + " suite/stub1"));
+            EasyMock.verify(mockBuildInfo);
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    /**
+     * Test for {@link TestMappingSuiteRunner#loadTests()} that when multi force-test-mapping-module
+     * are specified, tests would be filtered.
+     */
+    @Test
+    public void testLoadTestsWithMultiModules() throws Exception {
+        File tempDir = null;
+        try {
+            OptionSetter setter = new OptionSetter(mRunner);
+            setter.setOptionValue("test-mapping-test-group", "postsubmit");
+            setter.setOptionValue("force-test-mapping-module", "suite/stub1");
+            setter.setOptionValue("force-test-mapping-module", "suite/stub2");
+
+            tempDir = FileUtil.createTempDir("test_mapping");
+
+            File srcDir = FileUtil.createTempDir("src", tempDir);
+            String srcFile =
+                File.separator + TEST_DATA_DIR + File.separator + DISABLED_PRESUBMIT_TESTS;
+            InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, srcDir, DISABLED_PRESUBMIT_TESTS);
+
+            srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_1";
+            resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, srcDir, TEST_MAPPING);
+            File subDir = FileUtil.createTempDir("sub_dir", srcDir);
+            srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_2";
+            resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, subDir, TEST_MAPPING);
+
+            List<File> filesToZip =
+                Arrays.asList(srcDir, new File(tempDir, DISABLED_PRESUBMIT_TESTS));
+            File zipFile = Paths.get(tempDir.getAbsolutePath(), TEST_MAPPINGS_ZIP).toFile();
+            ZipUtil.createZip(filesToZip, zipFile);
+
+            IDeviceBuildInfo mockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
+            EasyMock.expect(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR))
+                .andReturn(null);
+            EasyMock.expect(mockBuildInfo.getTestsDir()).andReturn(new File("non-existing-dir"));
+            EasyMock.expect(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).andReturn(zipFile);
+
+            mRunner.setBuild(mockBuildInfo);
+            EasyMock.replay(mockBuildInfo);
+
+            LinkedHashMap<String, IConfiguration> configMap = mRunner.loadTests();
+            assertEquals(4, configMap.size());
+            assertTrue(configMap.containsKey(ABI_1 + " suite/stub1"));
+            assertTrue(configMap.containsKey(ABI_1 + " suite/stub2"));
+            assertTrue(configMap.containsKey(ABI_2 + " suite/stub1"));
+            assertTrue(configMap.containsKey(ABI_2 + " suite/stub2"));
+            EasyMock.verify(mockBuildInfo);
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    /**
+     * Test for {@link TestMappingSuiteRunner#getTestInfos(Set, String)} that when a module is
+     * specified, tests would be still found correctly.
+     */
+    @Test
+    public void testGetTestInfos() throws Exception {
+        Set<TestInfo> testInfos = new HashSet<>();
+        testInfos.add(createTestInfo("test", "path"));
+        testInfos.add(createTestInfo("test", "path2"));
+        testInfos.add(createTestInfo("test2", "path2"));
+
+        assertEquals(2, mRunner.getTestInfos(testInfos, "test").size());
+        assertEquals(1, mRunner.getTestInfos(testInfos, "test2").size());
+    }
+
+    /**
+     * Test for {@link TestMappingSuiteRunner#dedupTestInfos(Set)} that tests with the same test
+     * options would be filtered out.
+     */
+    @Test
+    public void testDedupTestInfos() throws Exception {
+        Set<TestInfo> testInfos = new HashSet<>();
+        testInfos.add(createTestInfo("test", "path"));
+        testInfos.add(createTestInfo("test", "path2"));
+        assertEquals(1, mRunner.dedupTestInfos(testInfos).size());
+
+        TestInfo anotherInfo = new TestInfo("test", "folder3", false);
+        anotherInfo.addOption(new TestOption("include-filter", "value1"));
+        testInfos.add(anotherInfo);
+        assertEquals(2, mRunner.dedupTestInfos(testInfos).size());
+    }
+
+    /**
+     * Test for {@link TestMappingSuiteRunner#getTestSources(Set)} that test sources would be found
+     * correctly.
+     */
+    @Test
+    public void testGetTestSources() throws Exception {
+        Set<TestInfo> testInfos = new HashSet<>();
+        testInfos.add(createTestInfo("test", "path"));
+        testInfos.add(createTestInfo("test", "path2"));
+        List<String> results = mRunner.getTestSources(testInfos);
+        assertEquals(2, results.size());
+    }
+
+    /**
+     * Test for {@link TestMappingSuiteRunner#parseOptions(TestInfo)} that the test options are
+     * injected correctly.
+     */
+    @Test
+    public void testParseOptions() throws Exception {
+        TestInfo info = createTestInfo("test", "path");
+        mRunner.parseOptions(info);
+        assertEquals(1, mRunner.getIncludeFilter().size());
+        assertEquals(1, mRunner.getExcludeFilter().size());
+    }
+
+    /**
+     * Test for {@link TestMappingSuiteRunner#createIndividualTests(Set, String)} that IRemoteTest
+     * object are created according to the test infos with different test options.
+     */
+    @Test
+    public void testCreateIndividualTestsWithDifferentTestInfos() throws Exception {
+        File tempDir = null;
+        try {
+            tempDir = FileUtil.createTempDir("tmp");
+            File moduleConfig = new File(tempDir, "module_name.config");
+            moduleConfig.createNewFile();
+            Set<TestInfo> testInfos = new HashSet<>();
+            testInfos.add(createTestInfo("test", "path"));
+            testInfos.add(createTestInfo("test2", "path"));
+            String configPath = moduleConfig.getAbsolutePath();
+            assertEquals(2, mRunner2.createIndividualTests(testInfos, configPath).size());
+            assertEquals(1, mRunner2.getIncludeFilter().size());
+            assertEquals(1, mRunner2.getExcludeFilter().size());
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    /**
+     * Test for {@link TestMappingSuiteRunner#createIndividualTests(Set, String)} that IRemoteTest
+     * object are created according to the test infos with multiple test options.
+     */
+    @Test
+    public void testCreateIndividualTestsWithDifferentTestOptions() throws Exception {
+        File tempDir = null;
+        try {
+            tempDir = FileUtil.createTempDir("tmp");
+            File moduleConfig = new File(tempDir, "module_name.config");
+            moduleConfig.createNewFile();
+            Set<TestInfo> testInfos = new HashSet<>();
+            testInfos.add(createTestInfo("test", "path"));
+            TestInfo info = new TestInfo("test", "path", false);
+            info.addOption(new TestOption("include-filter", "include-filter"));
+            testInfos.add(info);
+            String configPath = moduleConfig.getAbsolutePath();
+            assertEquals(2, mRunner2.createIndividualTests(testInfos, configPath).size());
+            assertEquals(1, mRunner2.getIncludeFilter().size());
+            assertEquals(0, mRunner2.getExcludeFilter().size());
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    /**
+     * Test for {@link TestMappingSuiteRunner#createIndividualTests(Set, String)} that IRemoteTest
+     * object are created according to the test infos with the same test options and name.
+     */
+    @Test
+    public void testCreateIndividualTestsWithSameTestInfos() throws Exception {
+        File tempDir = null;
+        try {
+            tempDir = FileUtil.createTempDir("tmp");
+            File moduleConfig = new File(tempDir, "module_name.config");
+            moduleConfig.createNewFile();
+            String configPath = moduleConfig.getAbsolutePath();
+            Set<TestInfo> testInfos = new HashSet<>();
+            testInfos.add(createTestInfo("test", "path"));
+            testInfos.add(createTestInfo("test", "path"));
+            assertEquals(1, mRunner2.createIndividualTests(testInfos, configPath).size());
+            assertEquals(1, mRunner2.getIncludeFilter().size());
+            assertEquals(1, mRunner2.getExcludeFilter().size());
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    /** Helper to create specific test infos. */
+    private TestInfo createTestInfo(String name, String source) {
+        TestInfo info = new TestInfo(name, source, false);
+        info.addOption(new TestOption("include-filter", name));
+        info.addOption(new TestOption("exclude-filter", name));
+        info.addOption(new TestOption("other", name));
+        return info;
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/suite/TestSuiteStub.java b/tests/src/com/android/tradefed/testtype/suite/TestSuiteStub.java
index 58a3e03..4e7aac2 100644
--- a/tests/src/com/android/tradefed/testtype/suite/TestSuiteStub.java
+++ b/tests/src/com/android/tradefed/testtype/suite/TestSuiteStub.java
@@ -86,6 +86,8 @@
             description = "The notAnnotation class name of the test name to run, can be repeated")
     private Set<String> mExcludeAnnotationFilter = new HashSet<>();
 
+    private Set<String> mExcludeFilters = new HashSet<>();
+
     /** Tests attempt. */
     private void testAttempt(ITestInvocationListener listener) throws DeviceNotAvailableException {
         listener.testRunStarted(mModule, 3);
@@ -224,10 +226,14 @@
     public void addAllIncludeFilters(Set<String> filters) {}
 
     @Override
-    public void addExcludeFilter(String filter) {}
+    public void addExcludeFilter(String filter) {
+        mExcludeFilters.add(filter);
+    }
 
     @Override
-    public void addAllExcludeFilters(Set<String> filters) {}
+    public void addAllExcludeFilters(Set<String> filters) {
+        mExcludeFilters.addAll(filters);
+    }
 
     @Override
     public void clearIncludeFilters() {}
@@ -239,11 +245,13 @@
 
     @Override
     public Set<String> getExcludeFilters() {
-        return new HashSet<>();
+        return mExcludeFilters;
     }
 
     @Override
-    public void clearExcludeFilters() {}
+    public void clearExcludeFilters() {
+        mExcludeFilters.clear();
+    }
 
     @Override
     public void addIncludeAnnotation(String annotation) {
diff --git a/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java b/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java
index 380e75c..000e565 100644
--- a/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java
@@ -347,6 +347,45 @@
         verify(mSuite).setExcludeFilter(excludeRun1);
     }
 
+    /** Ensure that the --module option can be used to force a single module to run. */
+    @Test
+    public void testReschedule_module_option() throws Exception {
+        OptionSetter setter = new OptionSetter(mTest);
+        setter.setOptionValue(BaseTestSuite.MODULE_OPTION, "run0");
+        populateFakeResults(2, 2, 1, 0, 0, false);
+        mMockLoader.init();
+        EasyMock.expect(mMockLoader.getCommandLine()).andReturn("previous_command");
+        EasyMock.expect(mMockFactory.createConfigurationFromArgs(EasyMock.anyObject()))
+                .andReturn(mRescheduledConfiguration);
+        EasyMock.expect(mMockLoader.loadPreviousRecord()).andReturn(mFakeRecord);
+
+        mRescheduledConfiguration.setTests(EasyMock.anyObject());
+        EasyMock.expectLastCall().times(1);
+
+        EasyMock.expect(mMockRescheduler.scheduleConfig(mRescheduledConfiguration)).andReturn(true);
+        EasyMock.replay(
+                mMockRescheduler,
+                mMockLoader,
+                mMockFactory,
+                mRescheduledConfiguration,
+                mMockCommandOptions);
+        mTest.run(null);
+        EasyMock.verify(
+                mMockRescheduler,
+                mMockLoader,
+                mMockFactory,
+                mRescheduledConfiguration,
+                mMockCommandOptions);
+
+        Set<String> excludeRun0 = new HashSet<>();
+        excludeRun0.add("run0 test.class#testPass0");
+        verify(mSuite).setExcludeFilter(excludeRun0);
+        Set<String> excludeRun1 = new HashSet<>();
+        // Only run0 was requested to run.
+        excludeRun1.add("run1");
+        verify(mSuite).setExcludeFilter(excludeRun1);
+    }
+
     /**
      * Test that if an exclude-filter is provided without abi, we are still able to exclude all the
      * matching modules for all abis.
@@ -391,6 +430,47 @@
         verify(mSuite).setExcludeFilter(excludeRun1);
     }
 
+    /** Ensure that --module works when abi are present. */
+    @Test
+    public void testReschedule_moduleOption_abi() throws Exception {
+        OptionSetter setter = new OptionSetter(mTest);
+        // We specify to exclude "run1"
+        setter.setOptionValue(BaseTestSuite.MODULE_OPTION, "run0");
+        populateFakeResults(2, 2, 1, 0, 0, false, new Abi("armeabi-v7a", "32"));
+        mMockLoader.init();
+        EasyMock.expect(mMockLoader.getCommandLine()).andReturn("previous_command");
+        EasyMock.expect(mMockFactory.createConfigurationFromArgs(EasyMock.anyObject()))
+                .andReturn(mRescheduledConfiguration);
+        EasyMock.expect(mMockLoader.loadPreviousRecord()).andReturn(mFakeRecord);
+
+        mRescheduledConfiguration.setTests(EasyMock.anyObject());
+        EasyMock.expectLastCall().times(1);
+
+        EasyMock.expect(mMockRescheduler.scheduleConfig(mRescheduledConfiguration)).andReturn(true);
+        EasyMock.replay(
+                mMockRescheduler,
+                mMockLoader,
+                mMockFactory,
+                mRescheduledConfiguration,
+                mMockCommandOptions);
+        mTest.run(null);
+        EasyMock.verify(
+                mMockRescheduler,
+                mMockLoader,
+                mMockFactory,
+                mRescheduledConfiguration,
+                mMockCommandOptions);
+
+        Set<String> excludeRun0 = new HashSet<>();
+        // Run with the abi are excluded
+        excludeRun0.add("armeabi-v7a run0 test.class#testPass0");
+        verify(mSuite).setExcludeFilter(excludeRun0);
+        Set<String> excludeRun1 = new HashSet<>();
+        // Even if run1 had failed test cases, it was excluded so it's not running.
+        excludeRun1.add("armeabi-v7a run1");
+        verify(mSuite).setExcludeFilter(excludeRun1);
+    }
+
     /** Test rescheduling a configuration when no parameterized tests previously failed. */
     @Test
     public void testReschedule_parameterized_nofail() throws Exception {
diff --git a/tests/src/com/android/tradefed/util/JavaCodeCoverageFlusherTest.java b/tests/src/com/android/tradefed/util/JavaCodeCoverageFlusherTest.java
new file mode 100644
index 0000000..a8cb7de
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/JavaCodeCoverageFlusherTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.contains;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+
+import com.google.common.collect.ImmutableList;
+
+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 java.util.List;
+
+/** Unit tests for {@link JavaCodeCoverageFlusher}. */
+@RunWith(JUnit4.class)
+public final class JavaCodeCoverageFlusherTest {
+
+    private static final String PS_OUTPUT =
+            "USER       PID   PPID  VSZ   RSS   WCHAN       PC  S NAME\n"
+                    + "bluetooth   123  1366  123   456   SyS_epoll+   0  S com.android.bluetooth\n"
+                    + "u0_a80     4567  1366  456   789   SyS_epoll+   0  S com.google.android.gms.persistent\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";
+
+    private static final String PM_LIST_PACKAGES_OUTPUT =
+            "package:com.android.bluetooth\n"
+                    + "package:com.google.android.gms.persistent\n"
+                    + "package:com.android.not.used\n"
+                    + "package:com.android.phone\n";
+
+    @Mock ITestDevice mMockDevice;
+
+    // Object under test
+    JavaCodeCoverageFlusher mFlusher;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void testResetAll_calledForAllProcesses() throws DeviceNotAvailableException {
+        mFlusher = new JavaCodeCoverageFlusher(mMockDevice, ImmutableList.of());
+
+        doReturn(PS_OUTPUT).when(mMockDevice).executeShellCommand("ps -e");
+        doReturn(PM_LIST_PACKAGES_OUTPUT)
+                .when(mMockDevice)
+                .executeShellCommand("pm list packages -a");
+        doReturn("123").when(mMockDevice).getProcessPid("com.android.bluetooth");
+        doReturn("4567").when(mMockDevice).getProcessPid("com.google.android.gms.persistent");
+        doReturn("890").when(mMockDevice).getProcessPid("com.android.phone");
+
+        mFlusher.resetCoverage();
+
+        verify(mMockDevice)
+                .executeShellCommand(
+                        "am attach-agent com.android.bluetooth "
+                                + "/system/lib/libdumpcoverage.so=reset");
+        verify(mMockDevice)
+                .executeShellCommand(
+                        "am attach-agent com.google.android.gms.persistent "
+                                + "/system/lib/libdumpcoverage.so=reset");
+        verify(mMockDevice)
+                .executeShellCommand(
+                        "am attach-agent com.android.phone /system/lib/libdumpcoverage.so=reset");
+
+        verify(mMockDevice).executeShellCommand("cmd coverage reset");
+    }
+
+    @Test
+    public void testResetSpecific_calledForSpecificProcesses() throws DeviceNotAvailableException {
+        mFlusher = new JavaCodeCoverageFlusher(mMockDevice, ImmutableList.of("com.android.phone"));
+
+        doReturn(PS_OUTPUT).when(mMockDevice).executeShellCommand("ps -e");
+        doReturn(PM_LIST_PACKAGES_OUTPUT)
+                .when(mMockDevice)
+                .executeShellCommand("pm list packages -a");
+        doReturn("123").when(mMockDevice).getProcessPid("com.android.phone");
+
+        mFlusher.resetCoverage();
+
+        verify(mMockDevice)
+                .executeShellCommand(
+                        "am attach-agent com.android.phone /system/lib/libdumpcoverage.so=reset");
+
+        verify(mMockDevice).executeShellCommand("cmd coverage reset");
+    }
+
+    @Test
+    public void testDumpAll_calledForAllProcesses() throws DeviceNotAvailableException {
+        mFlusher = new JavaCodeCoverageFlusher(mMockDevice, ImmutableList.of());
+
+        doReturn(PS_OUTPUT).when(mMockDevice).executeShellCommand("ps -e");
+        doReturn(PM_LIST_PACKAGES_OUTPUT)
+                .when(mMockDevice)
+                .executeShellCommand("pm list packages -a");
+        doReturn("123").when(mMockDevice).getProcessPid("com.android.bluetooth");
+        doReturn("4567").when(mMockDevice).getProcessPid("com.google.android.gms.persistent");
+        doReturn("890").when(mMockDevice).getProcessPid("com.android.phone");
+
+        List<String> coverageFiles = mFlusher.forceCoverageFlush();
+
+        assertThat(coverageFiles)
+                .containsExactly(
+                        "/data/misc/trace/com.android.bluetooth-123.ec",
+                        "/data/misc/trace/com.google.android.gms.persistent-4567.ec",
+                        "/data/misc/trace/com.android.phone-890.ec",
+                        "/data/misc/trace/system_server.ec");
+
+        verify(mMockDevice)
+                .executeShellCommand(
+                        "am attach-agent com.android.bluetooth /system/lib/libdumpcoverage.so="
+                                + "dump:/data/misc/trace/com.android.bluetooth-123.ec");
+        verify(mMockDevice)
+                .executeShellCommand(
+                        "am attach-agent com.google.android.gms.persistent "
+                                + "/system/lib/libdumpcoverage.so=dump:"
+                                + "/data/misc/trace/com.google.android.gms.persistent-4567.ec");
+        verify(mMockDevice)
+                .executeShellCommand(
+                        "am attach-agent com.android.phone /system/lib/libdumpcoverage.so="
+                                + "dump:/data/misc/trace/com.android.phone-890.ec");
+
+        verify(mMockDevice).executeShellCommand(contains("cmd coverage dump"));
+    }
+
+    @Test
+    public void testDumpSpecific_calledForSpecificProcesses() throws DeviceNotAvailableException {
+        mFlusher =
+                new JavaCodeCoverageFlusher(
+                        mMockDevice,
+                        ImmutableList.of(
+                                "com.android.bluetooth", "com.google.android.gms.persistent"));
+
+        doReturn(PM_LIST_PACKAGES_OUTPUT)
+                .when(mMockDevice)
+                .executeShellCommand("pm list packages -a");
+        doReturn("234").when(mMockDevice).getProcessPid("com.android.bluetooth");
+
+        // Request coverage for com.android.bluetooth (valid process) and
+        // com.google.android.gms.persistent (not a valid process, coverage is not flushed).
+        List<String> coverageFiles = mFlusher.forceCoverageFlush();
+
+        assertThat(coverageFiles)
+                .containsExactly(
+                        "/data/misc/trace/com.android.bluetooth-234.ec",
+                        "/data/misc/trace/system_server.ec");
+
+        verify(mMockDevice).executeShellCommand("pm list packages -a");
+        verify(mMockDevice)
+                .executeShellCommand(
+                        "am attach-agent com.android.bluetooth /system/lib/libdumpcoverage.so"
+                                + "=dump:/data/misc/trace/com.android.bluetooth-234.ec");
+        verify(mMockDevice).executeShellCommand(contains("cmd coverage dump"));
+    }
+}
diff --git a/tests/src/com/android/tradefed/util/NativeCodeCoverageFlusherTest.java b/tests/src/com/android/tradefed/util/NativeCodeCoverageFlusherTest.java
index 523655c..d5f627f 100644
--- a/tests/src/com/android/tradefed/util/NativeCodeCoverageFlusherTest.java
+++ b/tests/src/com/android/tradefed/util/NativeCodeCoverageFlusherTest.java
@@ -34,7 +34,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.lang.IllegalStateException;
 import java.util.List;
 
 @RunWith(JUnit4.class)
@@ -48,15 +47,14 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-
-        mFlusher = new NativeCodeCoverageFlusher(mMockDevice);
     }
 
     @Test
     public void testClearCoverageMeasurements_rmCommandCalled() throws DeviceNotAvailableException {
         doReturn(true).when(mMockDevice).isAdbRoot();
 
-        mFlusher.clearCoverageMeasurements();
+        mFlusher = new NativeCodeCoverageFlusher(mMockDevice, ImmutableList.of());
+        mFlusher.resetCoverage();
 
         // Verify that the rm command was executed.
         verify(mMockDevice).executeShellCommand("rm -rf /data/misc/trace/*");
@@ -67,7 +65,8 @@
         doReturn(false).when(mMockDevice).isAdbRoot();
 
         try {
-            mFlusher.clearCoverageMeasurements();
+            mFlusher = new NativeCodeCoverageFlusher(mMockDevice, ImmutableList.of());
+            mFlusher.resetCoverage();
             fail("Should have thrown an exception");
         } catch (IllegalStateException e) {
             // Expected
@@ -82,7 +81,8 @@
             throws DeviceNotAvailableException {
         doReturn(true).when(mMockDevice).isAdbRoot();
 
-        mFlusher.forceCoverageFlush(ImmutableList.of());
+        mFlusher = new NativeCodeCoverageFlusher(mMockDevice, ImmutableList.of());
+        mFlusher.forceCoverageFlush();
 
         // Verify that the flush command for all processes was called.
         verify(mMockDevice).executeShellCommand("kill -37 -1");
@@ -97,7 +97,8 @@
         doReturn("12").when(mMockDevice).getProcessPid(processes.get(0));
         doReturn("789").when(mMockDevice).getProcessPid(processes.get(1));
 
-        mFlusher.forceCoverageFlush(processes);
+        mFlusher = new NativeCodeCoverageFlusher(mMockDevice, processes);
+        mFlusher.forceCoverageFlush();
 
         // Verify that the flush command for the specific processes was called.
         verify(mMockDevice).executeShellCommand("kill -37 12 789");
@@ -108,7 +109,8 @@
         doReturn(false).when(mMockDevice).isAdbRoot();
 
         try {
-            mFlusher.forceCoverageFlush(ImmutableList.of("mediaserver"));
+            mFlusher = new NativeCodeCoverageFlusher(mMockDevice, ImmutableList.of("mediaserver"));
+            mFlusher.forceCoverageFlush();
             fail("Should have thrown an exception");
         } catch (IllegalStateException e) {
             // Expected
diff --git a/tests/src/com/android/tradefed/util/TarUtilTest.java b/tests/src/com/android/tradefed/util/TarUtilTest.java
index cc9c267..4870b4d 100644
--- a/tests/src/com/android/tradefed/util/TarUtilTest.java
+++ b/tests/src/com/android/tradefed/util/TarUtilTest.java
@@ -88,6 +88,24 @@
     }
 
     /**
+     * Test that {TarUtil#extractTarGzipToTemp(File, String)} can extract properly a tar.gz file.
+     */
+    @Test
+    public void testExtractTarGzipToTemp() throws Exception {
+        InputStream logTarGz = getClass().getResourceAsStream(EMMA_METADATA_RESOURCE_PATH);
+        File tarGzFile = FileUtil.createTempFile("extract_tar_gz_test", ".tar.gz");
+        File tempDir = null;
+        try {
+            FileUtil.writeToFile(logTarGz, tarGzFile);
+            tempDir = TarUtil.extractTarGzipToTemp(tarGzFile, "extract_tar_gz_test");
+            Assert.assertEquals(2, tempDir.list().length);
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+            FileUtil.deleteFile(tarGzFile);
+        }
+    }
+
+    /**
      * Test that {TarUtil#extractAndLog(ITestLogger, File, String)} can untar properly a tar file
      * and export its content.
      */
diff --git a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
index 56cfa8c..c1537ca 100644
--- a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
+++ b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
@@ -32,8 +32,11 @@
 
 import java.io.File;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
@@ -167,12 +170,10 @@
             assertEquals(0, tests.size());
 
             tests = TestMapping.getTests(mockBuildInfo, "presubmit", true, null);
-            assertEquals(1, tests.size());
+            assertEquals(2, tests.size());
             Set<String> names = new HashSet<String>();
             for (TestInfo test : tests) {
                 names.add(test.getName());
-                // Make sure the tests for `test1` are merged and no option is kept.
-                assertTrue(test.getOptions().isEmpty());
                 if (test.getName().equals("test1")) {
                     assertTrue(test.getHostOnly());
                 } else {
@@ -228,6 +229,93 @@
     }
 
     /**
+     * Test for {@link TestMapping#getAllTestMappingPaths(Path)} to get TEST_MAPPING files from
+     * child directory.
+     */
+    @Test
+    public void testGetAllTestMappingPaths_FromChildDirectory() throws Exception {
+        File tempDir = null;
+        try {
+            tempDir = FileUtil.createTempDir("test_mapping");
+            Path testMappingsRootPath = Paths.get(tempDir.getAbsolutePath());
+            File srcDir = FileUtil.createTempDir("src", tempDir);
+            String srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_1";
+            InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, srcDir, TEST_MAPPING);
+            File subDir = FileUtil.createTempDir("sub_dir", srcDir);
+            srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_2";
+            resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, subDir, TEST_MAPPING);
+
+            List<String> testMappingRelativePaths = new ArrayList<>();
+            Path relPath = testMappingsRootPath.relativize(Paths.get(subDir.getAbsolutePath()));
+            testMappingRelativePaths.add(relPath.toString());
+            TestMapping.setTestMappingPaths(testMappingRelativePaths);
+            Set<Path> paths = TestMapping.getAllTestMappingPaths(testMappingsRootPath);
+            assertEquals(2, paths.size());
+        } finally {
+            TestMapping.setTestMappingPaths(new ArrayList<>());
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    /**
+     * Test for {@link TestMapping#getAllTestMappingPaths(Path)} to get TEST_MAPPING files from
+     * parent directory.
+     */
+    @Test
+    public void testGetAllTestMappingPaths_FromParentDirectory() throws Exception {
+        File tempDir = null;
+        try {
+            tempDir = FileUtil.createTempDir("test_mapping");
+            Path testMappingsRootPath = Paths.get(tempDir.getAbsolutePath());
+            File srcDir = FileUtil.createTempDir("src", tempDir);
+            String srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_1";
+            InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, srcDir, TEST_MAPPING);
+            File subDir = FileUtil.createTempDir("sub_dir", srcDir);
+            srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_2";
+            resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, subDir, TEST_MAPPING);
+
+            List<String> testMappingRelativePaths = new ArrayList<>();
+            Path relPath = testMappingsRootPath.relativize(Paths.get(srcDir.getAbsolutePath()));
+            testMappingRelativePaths.add(relPath.toString());
+            TestMapping.setTestMappingPaths(testMappingRelativePaths);
+            Set<Path> paths = TestMapping.getAllTestMappingPaths(testMappingsRootPath);
+            assertEquals(1, paths.size());
+        } finally {
+            TestMapping.setTestMappingPaths(new ArrayList<>());
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    /**
+     * Test for {@link TestMapping#getAllTestMappingPaths(Path)} to fail when no TEST_MAPPING files
+     * found.
+     */
+    @Test(expected = RuntimeException.class)
+    public void testGetAllTestMappingPaths_NoFilesFound() throws Exception {
+        File tempDir = null;
+        try {
+            tempDir = FileUtil.createTempDir("test_mapping");
+            Path testMappingsRootPath = Paths.get(tempDir.getAbsolutePath());
+            File srcDir = FileUtil.createTempDir("src", tempDir);
+
+            List<String> testMappingRelativePaths = new ArrayList<>();
+            Path relPath = testMappingsRootPath.relativize(Paths.get(srcDir.getAbsolutePath()));
+            testMappingRelativePaths.add(relPath.toString());
+            TestMapping.setTestMappingPaths(testMappingRelativePaths);
+            // No TEST_MAPPING files should be found according to the srcDir, getAllTestMappingPaths
+            // method shall raise RuntimeException.
+            TestMapping.getAllTestMappingPaths(testMappingsRootPath);
+        } finally {
+            TestMapping.setTestMappingPaths(new ArrayList<>());
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    /**
      * Test for {@link TestInfo#merge()} for merging two TestInfo objects to fail when module names
      * are different.
      */
@@ -504,4 +592,35 @@
             FileUtil.recursiveDelete(tempDir);
         }
     }
+
+    /** Test for {@link TestMapping#removeComments()} for removing comments in TEST_MAPPING file. */
+    @Test
+    public void testRemoveComments() throws Exception {
+        String jsonString = getJsonStringByName("test_mapping_with_comments1");
+        String goldenString = getJsonStringByName("test_mapping_golden1");
+        assertEquals(TestMapping.removeComments(jsonString), goldenString);
+    }
+
+    /** Test for {@link TestMapping#removeComments()} for removing comments in TEST_MAPPING file. */
+    @Test
+    public void testRemoveComments2() throws Exception {
+        String jsonString = getJsonStringByName("test_mapping_with_comments2");
+        String goldenString = getJsonStringByName("test_mapping_golden2");
+        assertEquals(TestMapping.removeComments(jsonString), goldenString);
+    }
+
+    private String getJsonStringByName(String fileName) throws Exception  {
+        File tempDir = null;
+        try {
+            tempDir = FileUtil.createTempDir("test_mapping");
+            File srcDir = FileUtil.createTempDir("src", tempDir);
+            String srcFile = File.separator + TEST_DATA_DIR + File.separator + fileName;
+            InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, srcDir, TEST_MAPPING);
+            Path file = Paths.get(srcDir.getAbsolutePath(), TEST_MAPPING);
+            return String.join("\n", Files.readAllLines(file, StandardCharsets.UTF_8));
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
 }