Merge "Atest: Add 4 detailed events to metrics."
diff --git a/.gitignore b/.gitignore
index 81cd456..76080d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,7 @@
 .idea
 *.iml
 *.pyc
+
+# python coverage generated files.
+.coverage
+**/htmlcov
diff --git a/Android.bp b/Android.bp
index 1bb84d9..dbe9b42 100644
--- a/Android.bp
+++ b/Android.bp
@@ -24,6 +24,7 @@
       "-Xep:ConstantField:ERROR",
       "-Xep:DeadException:ERROR",
       "-Xep:EqualsIncompatibleType:ERROR",
+      "-Xep:ExtendingJUnitAssert:ERROR",
       "-Xep:FormatString:ERROR",
       "-Xep:GetClassOnClass:ERROR",
       "-Xep:IdentityBinaryExpression:ERROR",
@@ -32,9 +33,11 @@
       "-Xep:JUnitAmbiguousTestClass:ERROR",
       "-Xep:MissingFail:ERROR",
       "-Xep:MissingOverride:ERROR",
+      "-Xep:ModifiedButNotUsed:ERROR",
       "-Xep:MustBeClosedChecker:ERROR",
       "-Xep:Overrides:ERROR",
       "-Xep:PackageLocation:ERROR",
+      "-Xep:ParameterName:ERROR",
       "-Xep:ReferenceEquality:ERROR",
       "-Xep:RemoveUnusedImports:ERROR",
       "-Xep:ReturnValueIgnored:ERROR",
diff --git a/atest/OWNERS b/atest/OWNERS
index def5f76..e41a595 100644
--- a/atest/OWNERS
+++ b/atest/OWNERS
@@ -1,3 +1,3 @@
 dshi@google.com
 kevcheng@google.com
-mikehoran@google.com
+yangbill@google.com
diff --git a/atest/atest_integration_tests.py b/atest/atest_integration_tests.py
index 33fc767..1dcd12f 100755
--- a/atest/atest_integration_tests.py
+++ b/atest/atest_integration_tests.py
@@ -31,6 +31,7 @@
 
 import os
 import subprocess
+import sys
 import tempfile
 import time
 import unittest
@@ -45,7 +46,8 @@
     """ATest Integration Test Class."""
     NAME = 'ATestIntegrationTest'
     EXECUTABLE = 'atest'
-    _RUN_CMD = '{exe} {test}'
+    OPTIONS = ''
+    _RUN_CMD = '{exe} {options} {test}'
     _PASSED_CRITERIAS = ['will be rescheduled', 'All tests passed']
 
     def setUp(self):
@@ -64,7 +66,8 @@
         Args:
             testcase: A string of testcase name.
         """
-        run_cmd_dict = {'exe': self.EXECUTABLE, 'test': testcase}
+        run_cmd_dict = {'exe': self.EXECUTABLE, 'options': self.OPTIONS,
+                        'test': testcase}
         run_command = self._RUN_CMD.format(**run_cmd_dict)
         try:
             subprocess.check_output(run_command,
@@ -124,6 +127,10 @@
 
 
 if __name__ == '__main__':
+    # TODO(b/129029189) Implement detail comparison check for dry-run mode.
+    ARGS = ' '.join(sys.argv[1:])
+    if ARGS:
+        ATestIntegrationTest.OPTIONS = ARGS
     TEST_PLANS = os.path.join(os.path.dirname(__file__), _INTEGRATION_TESTS)
     try:
         LOG_PATH = os.path.join(create_test_run_dir(), _LOG_FILE)
diff --git a/atest/run_atest_unittests.sh b/atest/run_atest_unittests.sh
index b313d21..6759456 100755
--- a/atest/run_atest_unittests.sh
+++ b/atest/run_atest_unittests.sh
@@ -20,34 +20,53 @@
 #   2. PREUPLOAD hook invokes this script.
 
 ATEST_DIR=`dirname $0`/
+ATEST_REAL_PATH=`realpath $ATEST_DIR`
 PREUPLOAD_FILES=$@
 RED='\033[0;31m'
 GREEN='\033[0;32m'
 NC='\033[0m' # No Color
 
 function set_pythonpath() {
-  local path_to_check=`realpath $ATEST_DIR`
-  if ! echo $PYTHONPATH | grep -q $path_to_check; then
-    PYTHONPATH=$path_to_check:$PYTHONPATH
+  if ! echo $PYTHONPATH | grep -q $ATEST_REAL_PATH; then
+    PYTHONPATH=$ATEST_REAL_PATH:$PYTHONPATH
   fi
 }
 
+function print_summary() {
+    local test_results=$1
+    local coverage_run=$2
+    if [[ $coverage_run == "coverage" ]]; then
+        coverage report -m
+        coverage html
+    fi
+    if [[ $test_results -eq 0 ]]; then
+        echo -e "${GREEN}All unittests pass${NC}!"
+    else
+        echo -e "${RED}There was a unittest failure${NC}"
+    fi
+}
+
 function run_atest_unittests() {
-  set_pythonpath
+  echo "Running tests..."
+  local coverage_run=$1
+  local run_cmd="python"
   local rc=0
-  for test_file in $(find $ATEST_DIR -name "*_unittest.py");
-  do
-    if ! $test_file; then
+  set_pythonpath $coverage_run
+  if [[ $coverage_run == "coverage" ]]; then
+      # Clear previously coverage data.
+      python -m coverage erase
+      # Collected coverage data.
+      run_cmd="coverage run --source $ATEST_REAL_PATH --append"
+  fi
+
+  for test_file in $(find $ATEST_DIR -name "*_unittest.py"); do
+    if ! $run_cmd $test_file; then
       rc=1
+      echo -e "${RED}$t failed${NC}"
     fi
   done
-
   echo
-  if [[ $rc -eq 0 ]]; then
-    echo -e "${GREEN}All unittests pass${NC}!"
-  else
-    echo -e "${RED}There was a unittest failure${NC}"
-  fi
+  print_summary $rc $coverage_run
   return $rc
 }
 
@@ -57,8 +76,7 @@
   run_atest_unittests
   exit $?
 else
-  for f in $PREUPLOAD_FILES;
-  do
+  for f in $PREUPLOAD_FILES; do
     # We only want to run this unittest if atest files have been touched.
     if [[ $f == atest/* ]]; then
       run_atest_unittests
@@ -66,3 +84,12 @@
     fi
   done
 fi
+
+case "$1" in
+    'coverage')
+        run_atest_unittests "coverage"
+        ;;
+    *)
+        run_atest_unittests
+        ;;
+esac
diff --git a/error_prone_rules.mk b/error_prone_rules.mk
index 621e19a..fcc99bc 100644
--- a/error_prone_rules.mk
+++ b/error_prone_rules.mk
@@ -20,6 +20,7 @@
                           -Xep:ConstantField:ERROR \
                           -Xep:DeadException:ERROR \
                           -Xep:EqualsIncompatibleType:ERROR \
+                          -Xep:ExtendingJUnitAssert:ERROR \
                           -Xep:FormatString:ERROR \
                           -Xep:GetClassOnClass:ERROR \
                           -Xep:IdentityBinaryExpression:ERROR \
@@ -28,9 +29,11 @@
                           -Xep:JUnitAmbiguousTestClass:ERROR \
                           -Xep:MissingFail:ERROR \
                           -Xep:MissingOverride:ERROR \
+                          -Xep:ModifiedButNotUsed:ERROR \
                           -Xep:MustBeClosedChecker:ERROR \
                           -Xep:Overrides:ERROR \
                           -Xep:PackageLocation:ERROR \
+                          -Xep:ParameterName:ERROR \
                           -Xep:ReferenceEquality:ERROR \
                           -Xep:RemoveUnusedImports:ERROR \
                           -Xep:ReturnValueIgnored:ERROR \
diff --git a/res/config/suite/test_mapping_host_suite.xml b/res/config/suite/test_mapping_host_suite.xml
index 10dd50e..f2449f1 100644
--- a/res/config/suite/test_mapping_host_suite.xml
+++ b/res/config/suite/test_mapping_host_suite.xml
@@ -16,4 +16,8 @@
 <configuration description="Android test suite config for deviceless tests defined in TEST_MAPPING files">
     <option name="null-device" value="true" />
     <test class="com.android.tradefed.testtype.suite.TestMappingSuiteRunner"/>
+
+    <!-- Force GTest to report binary name in results -->
+    <option name="test-arg" value="com.android.tradefed.testtype.GTest:prepend-filename:true" />
+
 </configuration>
diff --git a/res/config/suite/test_mapping_suite.xml b/res/config/suite/test_mapping_suite.xml
index 4f2a5e1..e22fb7d 100644
--- a/res/config/suite/test_mapping_suite.xml
+++ b/res/config/suite/test_mapping_suite.xml
@@ -29,4 +29,7 @@
     <!-- Tell all HostTests to exclude certain annotations -->
     <option name="test-arg" value="com.android.tradefed.testtype.HostTest:exclude-annotation:android.platform.test.annotations.RestrictedBuildTest" />
     <option name="test-arg" value="com.android.compatibility.common.tradefed.testtype.JarHostTest:exclude-annotation:android.platform.test.annotations.RestrictedBuildTest" />
+
+    <!-- Force GTest to report binary name in results -->
+    <option name="test-arg" value="com.android.tradefed.testtype.GTest:prepend-filename:true" />
 </configuration>
diff --git a/src/com/android/tradefed/device/INativeDevice.java b/src/com/android/tradefed/device/INativeDevice.java
index 99c0ee1..0915e40 100644
--- a/src/com/android/tradefed/device/INativeDevice.java
+++ b/src/com/android/tradefed/device/INativeDevice.java
@@ -629,6 +629,14 @@
     public boolean doesFileExist(String deviceFilePath) throws DeviceNotAvailableException;
 
     /**
+     * Helper method to delete a file or directory on the device.
+     *
+     * @param deviceFilePath The absolute path of the file on the device.
+     * @throws DeviceNotAvailableException
+     */
+    public void deleteFile(String deviceFilePath) throws DeviceNotAvailableException;
+
+    /**
      * Retrieve a reference to a remote file on device.
      *
      * @param path the file path to retrieve. Can be an absolute path or path relative to '/'. (ie
diff --git a/src/com/android/tradefed/device/ITestDevice.java b/src/com/android/tradefed/device/ITestDevice.java
index 78e38f7..8356fe7 100644
--- a/src/com/android/tradefed/device/ITestDevice.java
+++ b/src/com/android/tradefed/device/ITestDevice.java
@@ -876,4 +876,13 @@
      * @throws DeviceNotAvailableException
      */
     public File dumpHeap(String process, String devicePath) throws DeviceNotAvailableException;
+
+    /**
+     * Collect the list of available displays id on the device as reported by "dumpsys
+     * SurfaceFlinger".
+     *
+     * @return The list of displays. Default always returns the default display 0.
+     * @throws DeviceNotAvailableException
+     */
+    public Set<Integer> listDisplayIds() throws DeviceNotAvailableException;
 }
diff --git a/src/com/android/tradefed/device/ManagedTestDeviceFactory.java b/src/com/android/tradefed/device/ManagedTestDeviceFactory.java
index 37d117f..284c53e 100644
--- a/src/com/android/tradefed/device/ManagedTestDeviceFactory.java
+++ b/src/com/android/tradefed/device/ManagedTestDeviceFactory.java
@@ -23,6 +23,7 @@
 import com.android.ddmlib.TimeoutException;
 import com.android.tradefed.device.DeviceManager.FastbootDevice;
 import com.android.tradefed.device.cloud.ManagedRemoteDevice;
+import com.android.tradefed.device.cloud.NestedDeviceStateMonitor;
 import com.android.tradefed.device.cloud.NestedRemoteDevice;
 import com.android.tradefed.device.cloud.RemoteAndroidVirtualDevice;
 import com.android.tradefed.device.cloud.VmRemoteDevice;
@@ -95,7 +96,8 @@
                 testDevice =
                         new NestedRemoteDevice(
                                 idevice,
-                                new DeviceStateMonitor(mDeviceManager, idevice, mFastbootEnabled),
+                                new NestedDeviceStateMonitor(
+                                        mDeviceManager, idevice, mFastbootEnabled),
                                 mAllocationMonitor);
             } else {
                 // Handle device connected via 'adb connect'
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index 2511d55..4d32f24 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -34,6 +34,7 @@
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.command.remote.DeviceDescriptor;
 import com.android.tradefed.config.GlobalConfiguration;
+import com.android.tradefed.device.contentprovider.ContentProviderHandler;
 import com.android.tradefed.host.IHostOptions;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -98,6 +99,7 @@
  */
 public class NativeDevice implements IManagedTestDevice {
 
+    private static final String SD_CARD = "/sdcard/";
     /**
      * Allow pauses of up to 2 minutes while receiving bugreport.
      * <p/>
@@ -210,6 +212,9 @@
     private String mLastConnectedWifiPsk = null;
     private boolean mNetworkMonitorEnabled = false;
 
+    private ContentProviderHandler mContentProvider = null;
+    private boolean mShouldSkipContentProviderSetup = false;
+
     /**
      * Interface for a generic device communication attempt.
      */
@@ -1039,6 +1044,14 @@
     @Override
     public boolean pushFile(final File localFile, final String remoteFilePath)
             throws DeviceNotAvailableException {
+        if (remoteFilePath.startsWith(SD_CARD)) {
+            ContentProviderHandler handler = getContentProvider();
+            if (handler != null) {
+                mShouldSkipContentProviderSetup = true;
+                return handler.pushFile(localFile, remoteFilePath);
+            }
+        }
+
         DeviceAction pushAction =
                 new DeviceAction() {
                     @Override
@@ -1104,15 +1117,19 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
-    public boolean doesFileExist(String destPath) throws DeviceNotAvailableException {
-        String lsGrep = executeShellCommand(String.format("ls \"%s\"", destPath));
+    public boolean doesFileExist(String deviceFilePath) throws DeviceNotAvailableException {
+        String lsGrep = executeShellCommand(String.format("ls \"%s\"", deviceFilePath));
         return !lsGrep.contains("No such file or directory");
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void deleteFile(String deviceFilePath) throws DeviceNotAvailableException {
+        executeShellCommand(String.format("rm -rf \"%s\"", deviceFilePath));
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -1840,7 +1857,11 @@
     /** Builds the OS command for the given adb shell command session and args */
     private String[] buildAdbShellCommand(String command) {
         // TODO: implement the shell v2 support in ddmlib itself.
-        String[] commandArgs = QuotationAwareTokenizer.tokenizeLine(command);
+        String[] commandArgs =
+                QuotationAwareTokenizer.tokenizeLine(
+                        command,
+                        /** No logging */
+                        false);
         return ArrayUtil.buildArray(
                 new String[] {"adb", "-s", getSerialNumber(), "shell"}, commandArgs);
     }
@@ -2336,7 +2357,7 @@
                             remoteFilePath.substring(0, remoteFilePath.lastIndexOf('/'));
                     if (!bugreportDir.isEmpty()) {
                         // clean bugreport files directory on device
-                        executeShellCommand(String.format("rm %s/*", bugreportDir));
+                        deleteFile(String.format("%s/*", bugreportDir));
                     }
 
                     return zipFile;
@@ -2770,11 +2791,8 @@
         doReboot();
         RecoveryMode cachedRecoveryMode = getRecoveryMode();
         setRecoveryMode(RecoveryMode.ONLINE);
-        if (mStateMonitor.waitForDeviceOnline() != null) {
-            enableAdbRoot();
-        } else {
-            recoverDevice();
-        }
+        waitForDeviceOnline();
+        enableAdbRoot();
         setRecoveryMode(cachedRecoveryMode);
     }
 
@@ -3966,7 +3984,22 @@
      */
     @Override
     public void postInvocationTearDown() {
-        // Default implementation empty on purpose
+        // Default implementation
+        if (getIDevice() instanceof StubDevice) {
+            return;
+        }
+        try {
+            // If we never installed it, don't even bother checking for it during tear down.
+            if (mContentProvider == null) {
+                return;
+            }
+            ContentProviderHandler handler = getContentProvider();
+            if (handler != null) {
+                handler.tearDown();
+            }
+        } catch (DeviceNotAvailableException e) {
+            CLog.e(e);
+        }
     }
 
     /**
@@ -4215,6 +4248,12 @@
         return null;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public Set<Integer> listDisplayIds() throws DeviceNotAvailableException {
+        throw new UnsupportedOperationException("dumpsys SurfaceFlinger is not supported.");
+    }
+
     /** Validate that pid is an integer and not empty. */
     private boolean checkValidPid(String output) {
         if (output.isEmpty()) {
@@ -4234,4 +4273,23 @@
     IHostOptions getHostOptions() {
         return GlobalConfiguration.getInstance().getHostOptions();
     }
+
+    /** Returns the {@link ContentProviderHandler} or null if not available. */
+    @VisibleForTesting
+    ContentProviderHandler getContentProvider() throws DeviceNotAvailableException {
+        // Prevent usage of content provider before API 25 as it would not work well.
+        if (getApiLevel() < 25) {
+            return null;
+        }
+        if (mContentProvider == null) {
+            mContentProvider = new ContentProviderHandler(this);
+        }
+        if (!mShouldSkipContentProviderSetup) {
+            boolean res = mContentProvider.setUp();
+            if (!res) {
+                return null;
+            }
+        }
+        return mContentProvider;
+    }
 }
diff --git a/src/com/android/tradefed/device/TestDevice.java b/src/com/android/tradefed/device/TestDevice.java
index 78a3f90..78b315e 100644
--- a/src/com/android/tradefed/device/TestDevice.java
+++ b/src/com/android/tradefed/device/TestDevice.java
@@ -26,6 +26,8 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ByteArrayInputStreamSource;
 import com.android.tradefed.result.InputStreamSource;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.KeyguardControllerState;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.StreamUtil;
@@ -62,7 +64,7 @@
     /** the command used to dismiss a error dialog. Currently sends a DPAD_CENTER key event */
     static final String DISMISS_DIALOG_CMD = "input keyevent 23";
     /** Commands that can be used to dismiss the keyguard. */
-    static final String DISMISS_KEYGUARD_CMD = "input keyevent 82";
+    public static final String DISMISS_KEYGUARD_CMD = "input keyevent 82";
 
     /**
      * Alternative command to dismiss the keyguard by requesting the Window Manager service to do
@@ -90,6 +92,8 @@
 
     /** user pattern in the output of "pm list users" = TEXT{<id>:<name>:<flags>} TEXT * */
     private static final String USER_PATTERN = "(.*?\\{)(\\d+)(:)(.*)(:)(\\d+)(\\}.*)";
+    /** Pattern to find the display ids of "dumpsys SurfaceFlinger" */
+    private static final String DISPLAY_ID_PATTERN = "(Display )(?<id>\\d+)( color modes:)";
 
     private static final int API_LEVEL_GET_CURRENT_USER = 24;
     /** Timeout to wait for a screenshot before giving up to avoid hanging forever */
@@ -343,9 +347,9 @@
 
     /**
      * Core implementation for installing application with split apk files {@link
-     * IDevice#installPackages(String, boolean, String...)}
-     * See "https://developer.android.com/studio/build/configure-apk-splits" on how to split
-     * apk to several files.
+     * IDevice#installPackages(List, boolean, List)} See
+     * "https://developer.android.com/studio/build/configure-apk-splits" on how to split apk to
+     * several files.
      *
      * @param packageFiles the local apk files
      * @param reinstall <code>true</code> if a reinstall should be performed
@@ -448,11 +452,10 @@
 
     /**
      * Core implementation for split apk remote installation {@link IDevice#installPackage(String,
-     * boolean, String...)}
-     * See "https://developer.android.com/studio/build/configure-apk-splits" on how to split
-     * apk to several files.
+     * boolean, String...)} See "https://developer.android.com/studio/build/configure-apk-splits" on
+     * how to split apk to several files.
      *
-     * @param packageFiles the remote apk file paths
+     * @param remoteApkPaths the remote apk file paths
      * @param reinstall <code>true</code> if a reinstall should be performed
      * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for
      *     available options.
@@ -1670,4 +1673,26 @@
         File dumpFile = pullFile(devicePath);
         return dumpFile;
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public Set<Integer> listDisplayIds() throws DeviceNotAvailableException {
+        Set<Integer> displays = new HashSet<>();
+        // Zero is the default display
+        displays.add(0);
+        CommandResult res = executeShellV2Command("dumpsys SurfaceFlinger | grep 'color modes:'");
+        if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
+            CLog.e("Something went wrong while listing displays: %s", res.getStderr());
+            return displays;
+        }
+        String output = res.getStdout();
+        Pattern p = Pattern.compile(DISPLAY_ID_PATTERN);
+        for (String line : output.split("\n")) {
+            Matcher m = p.matcher(line);
+            if (m.matches()) {
+                displays.add(Integer.parseInt(m.group("id")));
+            }
+        }
+        return displays;
+    }
 }
diff --git a/src/com/android/tradefed/device/cloud/NestedDeviceStateMonitor.java b/src/com/android/tradefed/device/cloud/NestedDeviceStateMonitor.java
new file mode 100644
index 0000000..a746129
--- /dev/null
+++ b/src/com/android/tradefed/device/cloud/NestedDeviceStateMonitor.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.device.cloud;
+
+import com.android.ddmlib.IDevice;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceStateMonitor;
+import com.android.tradefed.device.IDeviceManager;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.TestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.RunUtil;
+
+/**
+ * Device state monitor that executes extra checks on nested device to accommodate the specifics of
+ * the virtualized environment.
+ */
+public class NestedDeviceStateMonitor extends DeviceStateMonitor {
+
+    private ITestDevice mDevice;
+
+    public NestedDeviceStateMonitor(IDeviceManager mgr, IDevice device, boolean fastbootEnabled) {
+        super(mgr, device, fastbootEnabled);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean postOnlineCheck(final long waitTime) {
+        long startTime = System.currentTimeMillis();
+        if (!super.postOnlineCheck(waitTime)) {
+            return false;
+        }
+        long elapsedTime = System.currentTimeMillis() - startTime;
+        // Check that the device is actually usable and services have registered.
+        // VM devices tend to show back very quickly in adb but sometimes are not quite ready.
+        return nestedWaitForDeviceOnline(waitTime - elapsedTime);
+    }
+
+    private boolean nestedWaitForDeviceOnline(long maxWaitTime) {
+        long maxTime = System.currentTimeMillis() + maxWaitTime;
+        CommandResult res = null;
+        while (maxTime > System.currentTimeMillis()) {
+            try {
+                // TODO: Use IDevice directly
+                res = mDevice.executeShellV2Command(TestDevice.DISMISS_KEYGUARD_CMD);
+                if (CommandStatus.SUCCESS.equals(res.getStatus())) {
+                    return true;
+                }
+            } catch (DeviceNotAvailableException e) {
+                CLog.e(e);
+            }
+            RunUtil.getDefault().sleep(200L);
+        }
+        if (res != null) {
+            CLog.e("Error checking device ready: %s", res.getStderr());
+        }
+        return false;
+    }
+
+    /** Set the ITestDevice to call an adb command. */
+    final void setDevice(ITestDevice device) {
+        mDevice = device;
+    }
+}
diff --git a/src/com/android/tradefed/device/cloud/NestedRemoteDevice.java b/src/com/android/tradefed/device/cloud/NestedRemoteDevice.java
index 4d617f9..e1ef45f 100644
--- a/src/com/android/tradefed/device/cloud/NestedRemoteDevice.java
+++ b/src/com/android/tradefed/device/cloud/NestedRemoteDevice.java
@@ -38,5 +38,9 @@
     public NestedRemoteDevice(
             IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor) {
         super(device, stateMonitor, allocationMonitor);
+        // TODO: Use IDevice directly
+        if (stateMonitor instanceof NestedDeviceStateMonitor) {
+            ((NestedDeviceStateMonitor) stateMonitor).setDevice(this);
+        }
     }
 }
diff --git a/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java b/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
index a245dd8..902690b 100644
--- a/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
+++ b/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
@@ -42,6 +42,7 @@
     public static final String CONTENT_PROVIDER_URI = "content://android.tradefed.contentprovider";
     private static final String APK_NAME = "TradefedContentProvider.apk";
     private static final String CONTENT_PROVIDER_APK_RES = "/apks/contentprovider/" + APK_NAME;
+    private static final String PROPERTY_RESULT = "LEGACY_STORAGE: allow";
 
     private ITestDevice mDevice;
     private File mContentProviderApk = null;
@@ -56,20 +57,43 @@
      *
      * @return True if ready to be used, False otherwise.
      */
-    public boolean setUp() throws DeviceNotAvailableException, IOException {
+    public boolean setUp() throws DeviceNotAvailableException {
         Set<String> packageNames = mDevice.getInstalledPackageNames();
         if (packageNames.contains(PACKAGE_NAME)) {
             return true;
         }
         if (mContentProviderApk == null) {
-            mContentProviderApk = extractResourceApk();
+            try {
+                mContentProviderApk = extractResourceApk();
+            } catch (IOException e) {
+                CLog.e(e);
+                return false;
+            }
         }
         // Install package for all users
-        String output = mDevice.installPackage(mContentProviderApk, true, true);
-        if (output == null) {
+        String output =
+                mDevice.installPackage(
+                        mContentProviderApk,
+                        /** reinstall */
+                        true,
+                        /** grant permission */
+                        true);
+        if (output != null) {
+            CLog.e("Something went wrong while installing the content provider apk: %s", output);
+            FileUtil.deleteFile(mContentProviderApk);
+            return false;
+        }
+        // Enable appops legacy storage
+        mDevice.executeShellV2Command(
+                String.format("cmd appops set %s android:legacy_storage allow", PACKAGE_NAME));
+        // Check that it worked and set on the system
+        CommandResult appOpsResult =
+                mDevice.executeShellV2Command(String.format("cmd appops get %s", PACKAGE_NAME));
+        if (CommandStatus.SUCCESS.equals(appOpsResult.getStatus())
+                && appOpsResult.getStdout().contains(PROPERTY_RESULT)) {
             return true;
         }
-        CLog.e("Something went wrong while installing the content provider apk: %s", output);
+        CLog.e("Failed to set legacy_storage: %s", appOpsResult.getStderr());
         FileUtil.deleteFile(mContentProviderApk);
         return false;
     }
diff --git a/src/com/android/tradefed/device/metric/AtraceCollector.java b/src/com/android/tradefed/device/metric/AtraceCollector.java
index a49bd0c..628410b 100644
--- a/src/com/android/tradefed/device/metric/AtraceCollector.java
+++ b/src/com/android/tradefed/device/metric/AtraceCollector.java
@@ -272,7 +272,7 @@
                 }
 
                 if (!mPreserveOndeviceLog) {
-                    device.executeShellCommand("rm -f " + fullLogPath());
+                    device.deleteFile(fullLogPath());
                 }
                 else {
                     CLog.w("preserving ondevice atrace log: %s", fullLogPath());
diff --git a/src/com/android/tradefed/invoker/RemoteInvocationExecution.java b/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
index c8d663e..1c99346 100644
--- a/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
+++ b/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
@@ -352,8 +352,7 @@
         StringBuilder tfCmdBuilder =
                 new StringBuilder("TF_GLOBAL_CONFIG=" + globalConfig.getName());
         // Set an env variable to notify that this a remote environment.
-        // TODO: Reenable, right now it causes issue
-        // tfCmdBuilder.append(" " + REMOTE_VM_VARIABLE + "=1");
+        tfCmdBuilder.append(" " + REMOTE_VM_VARIABLE + "=1");
         tfCmdBuilder.append(" ENTRY_CLASS=" + CommandRunner.class.getCanonicalName());
         tfCmdBuilder.append(" ./tradefed.sh " + mRemoteTradefedDir + configFile.getName());
         if (config.getCommandOptions().shouldUseRemoteSandboxMode()) {
diff --git a/src/com/android/tradefed/targetprep/DeviceSetup.java b/src/com/android/tradefed/targetprep/DeviceSetup.java
index 9ee0cc7..0ad80f3 100644
--- a/src/com/android/tradefed/targetprep/DeviceSetup.java
+++ b/src/com/android/tradefed/targetprep/DeviceSetup.java
@@ -488,7 +488,7 @@
             if (mPreviousProperties != null) {
                 device.pushFile(mPreviousProperties, "/data/local.prop");
             } else {
-                device.executeShellCommand("rm -f /data/local.prop");
+                device.deleteFile("/data/local.prop");
             }
             device.reboot();
         }
diff --git a/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java b/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
index ae86527..3f2a58d 100644
--- a/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
@@ -54,14 +54,14 @@
     public void setUp(ITestDevice device, IBuildInfo buildInfo)
             throws TargetSetupError, DeviceNotAvailableException {
 
+        mApkInstalled = new ArrayList<>();
+        mTestApexInfoList = new ArrayList<ApexInfo>();
+
         if (getTestsFileName().isEmpty()) {
             CLog.i("No apk/apex module file to install. Skipping.");
             return;
         }
 
-        mApkInstalled = new ArrayList<>();
-        mTestApexInfoList = new ArrayList<ApexInfo>();
-
         // Clean up data/apex/active and data/app-staging.
         cleanUpStagedAndActiveSession(device, buildInfo);
 
diff --git a/src/com/android/tradefed/testtype/AndroidJUnitTest.java b/src/com/android/tradefed/testtype/AndroidJUnitTest.java
index ae934da..d9602b0 100644
--- a/src/com/android/tradefed/testtype/AndroidJUnitTest.java
+++ b/src/com/android/tradefed/testtype/AndroidJUnitTest.java
@@ -471,8 +471,7 @@
     }
 
     private void removeTestFilterDir() throws DeviceNotAvailableException {
-        ITestDevice device = getDevice();
-        device.executeShellCommand(String.format("rm -r %s", mTestFilterDir));
+        getDevice().deleteFile(mTestFilterDir);
     }
 
     private void reportEarlyFailure(ITestInvocationListener listener, String errorMessage) {
diff --git a/src/com/android/tradefed/testtype/DeviceJUnit4ClassRunner.java b/src/com/android/tradefed/testtype/DeviceJUnit4ClassRunner.java
index 3ab6c76..778f36f 100644
--- a/src/com/android/tradefed/testtype/DeviceJUnit4ClassRunner.java
+++ b/src/com/android/tradefed/testtype/DeviceJUnit4ClassRunner.java
@@ -25,6 +25,7 @@
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.testtype.MetricTestCase.LogHolder;
+import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -32,7 +33,9 @@
 import org.junit.rules.ExternalResource;
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
+import org.junit.runner.notification.RunNotifier;
 import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
 import org.junit.runners.model.InitializationError;
 import org.junit.runners.model.Statement;
 
@@ -61,6 +64,9 @@
     private IInvocationContext mContext;
     private Map<ITestDevice, IBuildInfo> mDeviceInfos;
 
+    /** Keep track of the list of downloaded files. */
+    private List<File> mDownloadedFiles = new ArrayList<>();
+
     @Option(name = HostTest.SET_OPTION_NAME, description = HostTest.SET_OPTION_DESC)
     private List<String> mKeyValueOptions = new ArrayList<>();
 
@@ -98,11 +104,22 @@
         }
         // Set options of test object
         HostTest.setOptionToLoadedObject(testObj, mKeyValueOptions);
-        resolveRemoteFileForObject(testObj);
+        mDownloadedFiles.addAll(resolveRemoteFileForObject(testObj));
         return testObj;
     }
 
     @Override
+    protected void runChild(FrameworkMethod method, RunNotifier notifier) {
+        try {
+            super.runChild(method, notifier);
+        } finally {
+            for (File f : mDownloadedFiles) {
+                FileUtil.recursiveDelete(f);
+            }
+        }
+    }
+
+    @Override
     public void setDevice(ITestDevice device) {
         mDevice = device;
     }
diff --git a/src/com/android/tradefed/testtype/GTest.java b/src/com/android/tradefed/testtype/GTest.java
index a86c96d..c3eb307 100644
--- a/src/com/android/tradefed/testtype/GTest.java
+++ b/src/com/android/tradefed/testtype/GTest.java
@@ -206,7 +206,7 @@
                 getMaxTestTimeMs() /* maxTimeToShellOutputResponse */,
                 TimeUnit.MILLISECONDS,
                 0 /* retry attempts */);
-        testDevice.executeShellCommand(String.format("rm %s", tmpFileDevice));
+        testDevice.deleteFile(tmpFileDevice);
     }
 
     @Override
diff --git a/src/com/android/tradefed/testtype/HostTest.java b/src/com/android/tradefed/testtype/HostTest.java
index ee05af3..0d3a55a 100644
--- a/src/com/android/tradefed/testtype/HostTest.java
+++ b/src/com/android/tradefed/testtype/HostTest.java
@@ -178,6 +178,9 @@
     private static final String TEST_FULL_NAME_FORMAT = "%s#%s";
     private static final String ROOT_DIR = "ROOT_DIR";
 
+    /** Track the downloaded files. */
+    private List<File> mDownloadedFiles = new ArrayList<>();
+
     public HostTest() {
         mFilterHelper = new TestFilterHelper(new ArrayList<String>(), new ArrayList<String>(),
                 mIncludeAnnotations, mExcludeAnnotations);
@@ -531,7 +534,19 @@
                 runRemoteTest(listener, test);
             } else if (Test.class.isAssignableFrom(classObj)) {
                 TestSuite junitTest = collectTests(collectClasses(classObj));
-                runJUnit3Tests(listener, junitTest, classObj.getName());
+                // Resolve dynamic files for the junit3 test objects
+                Enumeration<Test> allTest = junitTest.tests();
+                while (allTest.hasMoreElements()) {
+                    Test testObj = allTest.nextElement();
+                    mDownloadedFiles.addAll(resolveRemoteFileForObject(testObj));
+                }
+                try {
+                    runJUnit3Tests(listener, junitTest, classObj.getName());
+                } finally {
+                    for (File f : mDownloadedFiles) {
+                        FileUtil.recursiveDelete(f);
+                    }
+                }
             } else if (hasJUnit4Annotation(classObj)) {
                 // Include the method name filtering
                 Set<String> includes = mFilterHelper.getIncludeFilters();
@@ -717,7 +732,6 @@
                 if (testObj instanceof TestCase) {
                     ((TestCase)testObj).setName(method.getName());
                 }
-                resolveRemoteFileForObject(testObj);
                 suite.addTest(testObj);
             }
         }
diff --git a/src/com/android/tradefed/testtype/binary/ExecutableBaseTest.java b/src/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
index a15994a..b008ba8 100644
--- a/src/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
+++ b/src/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
@@ -20,6 +20,8 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.testtype.IAbi;
+import com.android.tradefed.testtype.IAbiReceiver;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.IRuntimeHintProvider;
 import com.android.tradefed.testtype.IShardableTest;
@@ -33,11 +35,11 @@
 
 /** Base class for executable style of tests. For example: binaries, shell scripts. */
 public abstract class ExecutableBaseTest
-        implements IRemoteTest, IRuntimeHintProvider, ITestCollector, IShardableTest {
+        implements IRemoteTest, IRuntimeHintProvider, ITestCollector, IShardableTest, IAbiReceiver {
 
-    public static final String NO_BINARY_ERROR = "Binary %s does not exists.";
+    public static final String NO_BINARY_ERROR = "Binary %s does not exist.";
 
-    @Option(name = "binary-path", description = "Path to the binary to be run. Can be repeated.")
+    @Option(name = "binary", description = "Path to the binary to be run. Can be repeated.")
     private List<String> mBinaryPaths = new ArrayList<>();
 
     @Option(
@@ -53,13 +55,15 @@
     )
     private long mRuntimeHintMs = 60000L; // 1 minute
 
+    private IAbi mAbi;
+
     @Override
     public final void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
-        for (String path : mBinaryPaths) {
-            // TODO: Improve the search logic to the usual places (ANDROID_TESTCASES, etc.)
-            if (!checkBinaryExists(path)) {
-                listener.testRunStarted(new File(path).getName(), 0);
-                listener.testRunFailed(String.format(NO_BINARY_ERROR, path));
+        for (String binary : mBinaryPaths) {
+            String path = findBinary(binary);
+            if (path == null) {
+                listener.testRunStarted(new File(binary).getName(), 0);
+                listener.testRunFailed(String.format(NO_BINARY_ERROR, binary));
                 listener.testRunEnded(0L, new HashMap<String, Metric>());
             } else {
                 listener.testRunStarted(new File(path).getName(), 1);
@@ -75,12 +79,12 @@
     }
 
     /**
-     * Ensures that a binary exists.
+     * Search for the binary to be able to run it.
      *
-     * @param binaryPath the path of the binary.
-     * @return True if the binary exists.
+     * @param binary the path of the binary or simply the binary name.
+     * @return The path to the binary, or null if not found.
      */
-    public abstract boolean checkBinaryExists(String binaryPath);
+    public abstract String findBinary(String binary);
 
     /**
      * Actually run the binary at the given path.
@@ -104,6 +108,18 @@
 
     /** {@inheritDoc} */
     @Override
+    public final void setAbi(IAbi abi) {
+        mAbi = abi;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public IAbi getAbi() {
+        return mAbi;
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public final Collection<IRemoteTest> split() {
         if (mBinaryPaths.size() <= 2) {
             return null;
diff --git a/src/com/android/tradefed/testtype/binary/ExecutableHostTest.java b/src/com/android/tradefed/testtype/binary/ExecutableHostTest.java
index 69b0539..00e0cb4 100644
--- a/src/com/android/tradefed/testtype/binary/ExecutableHostTest.java
+++ b/src/com/android/tradefed/testtype/binary/ExecutableHostTest.java
@@ -16,10 +16,16 @@
 package com.android.tradefed.testtype.binary;
 
 import com.android.annotations.VisibleForTesting;
+import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IDeviceBuildInfo;
 import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.StubDevice;
+import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.testtype.IBuildReceiver;
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
@@ -28,6 +34,7 @@
 import com.android.tradefed.util.RunUtil;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -36,7 +43,8 @@
  * the host binary might communicate to a device. If the received device is not a {@link StubDevice}
  * the serial will be passed to the binary to be used.
  */
-public class ExecutableHostTest extends ExecutableBaseTest implements IDeviceTest {
+@OptionClass(alias = "executable-host-test")
+public class ExecutableHostTest extends ExecutableBaseTest implements IDeviceTest, IBuildReceiver {
 
     private static final String ANDROID_SERIAL = "ANDROID_SERIAL";
 
@@ -48,10 +56,40 @@
     private long mTimeoutPerBinaryMs = 5 * 60 * 1000L;
 
     private ITestDevice mDevice;
+    private IBuildInfo mBuild;
 
     @Override
-    public boolean checkBinaryExists(String binaryPath) {
-        return new File(binaryPath).exists();
+    public String findBinary(String binary) {
+        File bin = new File(binary);
+        // If it's a local path or absolute path
+        if (bin.exists()) {
+            return bin.getAbsolutePath();
+        }
+        if (mBuild instanceof IDeviceBuildInfo) {
+            IDeviceBuildInfo deviceBuild = (IDeviceBuildInfo) mBuild;
+            File testsDir = deviceBuild.getTestsDir();
+
+            List<File> scanDirs = new ArrayList<>();
+            // If it exists, always look first in the ANDROID_HOST_OUT_TESTCASES
+            File targetTestCases = deviceBuild.getFile(BuildInfoFileKey.HOST_LINKED_DIR);
+            if (targetTestCases != null) {
+                scanDirs.add(targetTestCases);
+            }
+            if (testsDir != null) {
+                scanDirs.add(testsDir);
+            }
+
+            try {
+                // Search the full tests dir if no target dir is available.
+                File src = FileUtil.findFile(binary, getAbi(), scanDirs.toArray(new File[] {}));
+                if (src != null) {
+                    return src.getAbsolutePath();
+                }
+            } catch (IOException e) {
+                CLog.e("Failed to find test files from directory.");
+            }
+        }
+        return null;
     }
 
     @Override
@@ -93,6 +131,11 @@
         return mDevice;
     }
 
+    @Override
+    public final void setBuild(IBuildInfo buildInfo) {
+        mBuild = buildInfo;
+    }
+
     @VisibleForTesting
     IRunUtil createRunUtil() {
         return new RunUtil();
diff --git a/src/com/android/tradefed/util/QuotationAwareTokenizer.java b/src/com/android/tradefed/util/QuotationAwareTokenizer.java
index e72afa7..eaf7377 100644
--- a/src/com/android/tradefed/util/QuotationAwareTokenizer.java
+++ b/src/com/android/tradefed/util/QuotationAwareTokenizer.java
@@ -25,34 +25,40 @@
     private static final String LOG_TAG = "TOKEN";
 
     /**
-     * Tokenizes the string, splitting on specified delimiter.  Does not split between consecutive,
+     * Tokenizes the string, splitting on specified delimiter. Does not split between consecutive,
      * unquoted double-quote marks.
-     * <p/>
-     * How the tokenizer works:
+     *
+     * <p>How the tokenizer works:
+     *
      * <ol>
-     *     <li> Split the string into "characters" where each "character" is either an escaped
-     *          character like \" (that is, "\\\"") or a single real character like f (just "f").
-     *     <li> For each "character"
-     *     <ol>
+     *   <li> Split the string into "characters" where each "character" is either an escaped
+     *       character like \" (that is, "\\\"") or a single real character like f (just "f").
+     *   <li> For each "character"
+     *       <ol>
      *         <li> If it's a space, finish a token unless we're being quoted
      *         <li> If it's a quotation mark, flip the "we're being quoted" bit
      *         <li> Otherwise, add it to the token being built
-     *     </ol>
-     *     <li> At EOL, we typically haven't added the final token to the (tokens) {@link ArrayList}
-     *     <ol>
+     *       </ol>
+     *
+     *   <li> At EOL, we typically haven't added the final token to the (tokens) {@link ArrayList}
+     *       <ol>
      *         <li> If the last "character" is an escape character, throw an exception; that's not
-     *              valid
+     *             valid
      *         <li> If we're in the middle of a quotation, throw an exception; that's not valid
      *         <li> Otherwise, add the final token to (tokens)
-     *     </ol>
-     *     <li> Return a String[] version of (tokens)
+     *       </ol>
+     *
+     *   <li> Return a String[] version of (tokens)
      * </ol>
      *
      * @param line A {@link String} to be tokenized
+     * @param delim the delimiter to split on
+     * @param logging whether or not to log operations
      * @return A tokenized version of the string
      * @throws IllegalArgumentException if the line cannot be parsed
      */
-    public static String[] tokenizeLine(String line, String delim) throws IllegalArgumentException {
+    public static String[] tokenizeLine(String line, String delim, boolean logging)
+            throws IllegalArgumentException {
         if (line == null) {
             throw new IllegalArgumentException("line is null");
         }
@@ -65,7 +71,7 @@
         String aChar = "";
         boolean quotation = false;
 
-        Log.d(LOG_TAG, String.format("Trying to tokenize the line '%s'", line));
+        log(String.format("Trying to tokenize the line '%s'", line), logging);
         while (charMatcher.find()) {
             aChar = charMatcher.group();
 
@@ -77,7 +83,7 @@
                     if (token.length() > 0) {
                         // this is the end of a non-empty token; dump it in our list of tokens,
                         // clear our temp storage, and keep rolling
-                        Log.d(LOG_TAG, String.format("Finished token '%s'", token.toString()));
+                        log(String.format("Finished token '%s'", token.toString()), logging);
                         tokens.add(token.toString());
                         token.delete(0, token.length());
                     }
@@ -85,7 +91,7 @@
                 }
             } else if ("\"".equals(aChar)) {
                 // unescaped quotation mark; flip quotation state
-                Log.v(LOG_TAG, "Flipped quotation state");
+                log("Flipped quotation state", logging);
                 quotation ^= true;
             } else {
                 // default case: add the character to the token being built
@@ -101,7 +107,7 @@
 
         // Add the final token to the tokens array.
         if (token.length() > 0) {
-            Log.v(LOG_TAG, String.format("Finished final token '%s'", token.toString()));
+            log(String.format("Finished final token '%s'", token.toString()), logging);
             tokens.add(token.toString());
             token.delete(0, token.length());
         }
@@ -117,7 +123,22 @@
      * See also {@link #tokenizeLine(String, String)}
      */
     public static String[] tokenizeLine(String line) throws IllegalArgumentException {
-        return tokenizeLine(line, " ");
+        return tokenizeLine(line, " ", true);
+    }
+
+    public static String[] tokenizeLine(String line, String delim) throws IllegalArgumentException {
+        return tokenizeLine(line, delim, true);
+    }
+
+    /**
+     * Tokenizes the string, splitting on spaces. Does not split between consecutive, unquoted
+     * double-quote marks.
+     *
+     * <p>See also {@link #tokenizeLine(String, String)}
+     */
+    public static String[] tokenizeLine(String line, boolean logging)
+            throws IllegalArgumentException {
+        return tokenizeLine(line, " ", logging);
     }
 
     /**
@@ -147,4 +168,10 @@
         }
         return sb.toString();
     }
+
+    private static void log(String message, boolean display) {
+        if (display) {
+            Log.v(LOG_TAG, message);
+        }
+    }
 }
diff --git a/src/com/android/tradefed/util/RunUtil.java b/src/com/android/tradefed/util/RunUtil.java
index 15b7d86..593559b 100644
--- a/src/com/android/tradefed/util/RunUtil.java
+++ b/src/com/android/tradefed/util/RunUtil.java
@@ -250,8 +250,8 @@
                 new RunnableResult(
                         /* input= */ null,
                         createProcessBuilder(command),
-                        /* stdout= */ null,
-                        /* stderr= */ null,
+                        /* stdoutStream= */ null,
+                        /* stderrStream= */ null,
                         inputRedirect);
         CommandStatus status = runTimed(timeout, osRunnable, true);
         CommandResult result = osRunnable.getResult();
diff --git a/src/com/android/tradefed/util/testmapping/TestInfo.java b/src/com/android/tradefed/util/testmapping/TestInfo.java
index ee14503..8795138 100644
--- a/src/com/android/tradefed/util/testmapping/TestInfo.java
+++ b/src/com/android/tradefed/util/testmapping/TestInfo.java
@@ -259,25 +259,54 @@
     }
 
     @Override
+    public boolean equals(Object o) {
+        return this.toString().equals(o.toString());
+    }
+
+    @Override
+    public int hashCode() {
+        return this.toString().hashCode();
+    }
+
+    @Override
     public String toString() {
-        String options = "";
-        String keywords = "";
+        StringBuilder string = new StringBuilder();
+        string.append(mName);
         if (!mOptions.isEmpty()) {
-            options =
+            String options =
                     String.format(
-                            "; Options: %s",
+                            "Options: %s",
                             Joiner.on(",")
                                     .join(
                                             mOptions.stream()
+                                                    .sorted()
                                                     .map(TestOption::toString)
                                                     .collect(Collectors.toList())));
+            string.append("\n\t").append(options);
         }
         if (!mKeywords.isEmpty()) {
-            keywords =
+            String keywords =
                     String.format(
-                            "; Keywords: %s",
-                            Joiner.on(",").join(mKeywords.stream().collect(Collectors.toList())));
+                            "Keywords: %s",
+                            Joiner.on(",")
+                                    .join(
+                                            mKeywords.stream()
+                                                    .sorted()
+                                                    .collect(Collectors.toList())));
+            string.append("\n\t").append(keywords);
         }
-        return String.format("%s%s%s", mName, options, keywords);
+        if (!mSources.isEmpty()) {
+            String sources =
+                    String.format(
+                            "Sources: %s",
+                            Joiner.on(",")
+                                    .join(
+                                            mSources.stream()
+                                                    .sorted()
+                                                    .collect(Collectors.toList())));
+            string.append("\n\t").append(sources);
+        }
+        string.append("\n\tHost: ").append(mHostOnly);
+        return string.toString();
     }
 }
diff --git a/src/com/android/tradefed/util/testmapping/TestMapping.java b/src/com/android/tradefed/util/testmapping/TestMapping.java
index cf7ed92..79da7c2 100644
--- a/src/com/android/tradefed/util/testmapping/TestMapping.java
+++ b/src/com/android/tradefed/util/testmapping/TestMapping.java
@@ -20,6 +20,8 @@
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.ZipUtil2;
 
+import com.google.common.annotations.VisibleForTesting;
+
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -32,8 +34,8 @@
 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;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
@@ -57,9 +59,10 @@
     private static final String KEY_OPTIONS = "options";
     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";
+    // A file containing module names that are disabled in presubmit test runs.
+    private static final String DISABLED_PRESUBMIT_TESTS_FILE = "disabled-presubmit-tests";
 
-    private Map<String, List<TestInfo>> mTestCollection = null;
+    private Map<String, Set<TestInfo>> mTestCollection = null;
 
     /**
      * Constructor to create a {@link TestMapping} object from a path to TEST_MAPPING file.
@@ -84,7 +87,7 @@
                         // need to be considered.
                         continue;
                     }
-                    List<TestInfo> testsForGroup = new ArrayList<TestInfo>();
+                    Set<TestInfo> testsForGroup = new HashSet<>();
                     mTestCollection.put(group, testsForGroup);
                     JSONArray arr = root.getJSONArray(group);
                     for (int i = 0; i < arr.length(); i++) {
@@ -144,13 +147,13 @@
      *     returned. false to return tests that require device to run.
      * @param keywords A set of {@link String} to be matched when filtering tests to run in a Test
      *     Mapping suite.
-     * @return A {@code List<TestInfo>} of the test infos.
+     * @return A {@code Set<TestInfo>} of the test infos.
      */
-    public List<TestInfo> getTests(
+    public Set<TestInfo> getTests(
             String testGroup, Set<String> disabledTests, boolean hostOnly, Set<String> keywords) {
-        List<TestInfo> tests = new ArrayList<TestInfo>();
+        Set<TestInfo> tests = new HashSet<TestInfo>();
 
-        for (TestInfo test : mTestCollection.getOrDefault(testGroup, new ArrayList<TestInfo>())) {
+        for (TestInfo test : mTestCollection.getOrDefault(testGroup, new HashSet<>())) {
             if (disabledTests != null && disabledTests.contains(test.getName())) {
                 CLog.d("Test is disabled: %s.", test);
                 continue;
@@ -228,23 +231,11 @@
     public static Set<TestInfo> getTests(
             IBuildInfo buildInfo, String testGroup, boolean hostOnly, Set<String> keywords) {
         Set<TestInfo> tests = new HashSet<TestInfo>();
-        Set<String> disabledTests = new HashSet<>();
-
-        File testMappingsZip = buildInfo.getFile(TEST_MAPPINGS_ZIP);
-        File testMappingsDir = null;
+        File testMappingsDir = extractTestMappingsZip(buildInfo.getFile(TEST_MAPPINGS_ZIP));
         Stream<Path> stream = null;
         try {
-            testMappingsDir = ZipUtil2.extractZipToTemp(testMappingsZip, TEST_MAPPINGS_ZIP);
             Path testMappingsRootPath = Paths.get(testMappingsDir.getAbsolutePath());
-            if (testGroup.equals(PRESUBMIT)) {
-                File disabledPresubmitTestsFile =
-                        new File(testMappingsRootPath.toString(), DISABLED_PRESUBMIT_TESTS);
-                disabledTests.addAll(
-                        Arrays.asList(
-                                FileUtil.readStringFromFile(disabledPresubmitTestsFile)
-                                        .split("\\r?\\n")));
-            }
-
+            Set<String> disabledTests = getDisabledTests(testMappingsRootPath, testGroup);
             stream = Files.walk(testMappingsRootPath, FileVisitOption.FOLLOW_LINKS);
             stream.filter(path -> path.getFileName().toString().equals(TEST_MAPPING))
                     .forEach(
@@ -258,13 +249,10 @@
                                                             keywords)));
 
         } catch (IOException e) {
-            RuntimeException runtimeException =
-                    new RuntimeException(
-                            String.format(
-                                    "IO error (%s) when reading tests from TEST_MAPPING files (%s)",
-                                    e.getMessage(), testMappingsZip.getAbsolutePath()),
-                            e);
-            throw runtimeException;
+            throw new RuntimeException(
+                    String.format(
+                            "IO exception (%s) when reading tests from TEST_MAPPING files (%s)",
+                            e.getMessage(), testMappingsDir.getAbsolutePath()), e);
         } finally {
             if (stream != null) {
                 stream.close();
@@ -274,4 +262,111 @@
 
         return TestMapping.mergeTests(tests);
     }
+
+    /**
+     * Helper to find all tests in the TEST_MAPPING files from a given directory.
+     *
+     * @param testMappingsDir the {@link File} the directory containing all Test Mapping files.
+     * @return A {@code Map<String, Set<TestInfo>>} of tests in the given directory and its child
+     *     directories.
+     */
+    public static Map<String, Set<TestInfo>> getAllTests(File testMappingsDir) {
+        Map<String, Set<TestInfo>> allTests = new HashMap<String, Set<TestInfo>>();
+        Stream<Path> stream = null;
+        try {
+            Path testMappingsRootPath = Paths.get(testMappingsDir.getAbsolutePath());
+            stream = Files.walk(testMappingsRootPath, FileVisitOption.FOLLOW_LINKS);
+            stream.filter(path -> path.getFileName().toString().equals(TEST_MAPPING))
+                    .forEach(
+                            path ->
+                                    getAllTests(allTests, path, testMappingsRootPath));
+
+        } catch (IOException e) {
+            throw new RuntimeException(
+                    String.format(
+                            "IO exception (%s) when reading tests from TEST_MAPPING files (%s)",
+                            e.getMessage(), testMappingsDir.getAbsolutePath()), e);
+        } finally {
+            if (stream != null) {
+                stream.close();
+            }
+        }
+        return allTests;
+    }
+
+    /**
+     * Extract a zip file and return the directory that contains the content of unzipped files.
+     *
+     * @param testMappingsZip A {@link File} of the test mappings zip to extract.
+     * @return a {@link File} pointing to the temp directory for test mappings zip.
+     */
+    public static File extractTestMappingsZip(File testMappingsZip) {
+        File testMappingsDir = null;
+        try {
+            testMappingsDir = ZipUtil2.extractZipToTemp(testMappingsZip, TEST_MAPPINGS_ZIP);
+        } catch (IOException e) {
+            throw new RuntimeException(
+                    String.format(
+                            "IO exception (%s) when extracting test mappings zip (%s)",
+                            e.getMessage(), testMappingsZip.getAbsolutePath()), e);
+        }
+        return testMappingsDir;
+    }
+
+    /**
+     * Get disabled tests from test mapping artifact.
+     *
+     * @param testMappingsRootPath The {@link Path} to a test mappings zip path.
+     * @param testGroup a {@link String} of the test group.
+     * @return a {@link Set<String>} containing all the disabled presubmit tests. No test is
+     *     returned if the testGroup is not PRESUBMIT.
+     */
+    @VisibleForTesting
+    static Set<String> getDisabledTests(Path testMappingsRootPath, String testGroup) {
+        Set<String> disabledTests = new HashSet<>();
+        File disabledPresubmitTestsFile =
+                new File(testMappingsRootPath.toString(), DISABLED_PRESUBMIT_TESTS_FILE);
+        if (!(testGroup.equals(PRESUBMIT) && disabledPresubmitTestsFile.exists())) {
+            return disabledTests;
+        }
+        try {
+            disabledTests.addAll(
+                    Arrays.asList(
+                            FileUtil.readStringFromFile(disabledPresubmitTestsFile)
+                                    .split("\\r?\\n")));
+        } catch (IOException e) {
+            throw new RuntimeException(
+                    String.format(
+                            "IO exception (%s) when reading disabled tests from file (%s)",
+                            e.getMessage(), disabledPresubmitTestsFile.getAbsolutePath()), e);
+        }
+        return disabledTests;
+    }
+
+    /**
+     * Helper to find all tests in the TEST_MAPPING files from a given directory.
+     *
+     * @param allTests the {@code HashMap<String, Set<TestInfo>>} containing the tests of each
+     * test group.
+     * @param path the {@link Path} to a TEST_MAPPING file.
+     * @param testMappingsRootPath the {@link Path} to a test mappings zip path.
+     */
+    private static void getAllTests(Map<String, Set<TestInfo>> allTests,
+        Path path, Path testMappingsRootPath) {
+        Map<String, Set<TestInfo>> testCollection =
+            new TestMapping(path, testMappingsRootPath).getTestCollection();
+        for (String group : testCollection.keySet()) {
+            allTests.computeIfAbsent(group, k -> new HashSet<>()).addAll(testCollection.get(group));
+        }
+    }
+
+    /**
+     * Helper to get the test collection in a TEST_MAPPING file.
+     *
+     * @return A {@code Map<String, Set<TestInfo>>} containing the test collection in a
+     *     TEST_MAPPING file.
+     */
+    private Map<String, Set<TestInfo>> getTestCollection() {
+        return mTestCollection;
+    }
 }
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index ea67a59..d87c5b9 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -214,6 +214,7 @@
 import com.android.tradefed.testtype.AndroidJUnitTestTest;
 import com.android.tradefed.testtype.CodeCoverageListenerTest;
 import com.android.tradefed.testtype.DeviceBatteryLevelCheckerTest;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunnerTest;
 import com.android.tradefed.testtype.DeviceSuiteTest;
 import com.android.tradefed.testtype.DeviceTestCaseTest;
 import com.android.tradefed.testtype.DeviceTestSuiteTest;
@@ -624,6 +625,7 @@
     CodeCoverageListenerTest.class,
     CoverageMeasurementForwarderTest.class,
     DeviceBatteryLevelCheckerTest.class,
+    DeviceJUnit4ClassRunnerTest.class,
     DeviceSuiteTest.class,
     DeviceTestCaseTest.class,
     DeviceTestSuiteTest.class,
diff --git a/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java b/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
index 17c4133..87a1762 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
@@ -886,8 +886,6 @@
         final String configName = "template-include-config-with-default";
         final String targetName = "local-config";
         final String nameTemplate = "target";
-        Map<String, String> expected = new HashMap<String,String>();
-        expected.put(nameTemplate, targetName);
         IConfiguration tmp = null;
         try {
             tmp = mFactory.createConfigurationFromArgs(new String[]{configName,
diff --git a/tests/src/com/android/tradefed/device/NativeDeviceTest.java b/tests/src/com/android/tradefed/device/NativeDeviceTest.java
index f9f11d3..9074b85 100644
--- a/tests/src/com/android/tradefed/device/NativeDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/NativeDeviceTest.java
@@ -1210,34 +1210,44 @@
     /** Unit test for {@link NativeDevice#getBugreportz()}. */
     @Test
     public void testGetBugreportz() throws IOException {
-        mTestDevice = new TestableAndroidNativeDevice() {
-            @Override
-            public void executeShellCommand(
-                    String command, IShellOutputReceiver receiver,
-                    long maxTimeToOutputShellResponse, TimeUnit timeUnit, int retryAttempts)
+        mTestDevice =
+                new TestableAndroidNativeDevice() {
+                    @Override
+                    public void executeShellCommand(
+                            String command,
+                            IShellOutputReceiver receiver,
+                            long maxTimeToOutputShellResponse,
+                            TimeUnit timeUnit,
+                            int retryAttempts)
                             throws DeviceNotAvailableException {
-                String fakeRep = "OK:/data/0/com.android.shell/bugreports/bugreport1970-10-27.zip";
-                receiver.addOutput(fakeRep.getBytes(), 0, fakeRep.getBytes().length);
-            }
-            @Override
-            public boolean doesFileExist(String destPath) throws DeviceNotAvailableException {
-                return true;
-            }
-            @Override
-            public boolean pullFile(String remoteFilePath, File localFile)
-                    throws DeviceNotAvailableException {
-                return true;
-            }
-            @Override
-            public String executeShellCommand(String command) throws DeviceNotAvailableException {
-                assertEquals("rm /data/0/com.android.shell/bugreports/*", command);
-                return null;
-            }
-            @Override
-            public int getApiLevel() throws DeviceNotAvailableException {
-                return 24;
-            }
-        };
+                        String fakeRep =
+                                "OK:/data/0/com.android.shell/bugreports/bugreport1970-10-27.zip";
+                        receiver.addOutput(fakeRep.getBytes(), 0, fakeRep.getBytes().length);
+                    }
+
+                    @Override
+                    public boolean doesFileExist(String destPath)
+                            throws DeviceNotAvailableException {
+                        return true;
+                    }
+
+                    @Override
+                    public boolean pullFile(String remoteFilePath, File localFile)
+                            throws DeviceNotAvailableException {
+                        return true;
+                    }
+
+                    @Override
+                    public void deleteFile(String deviceFilePath)
+                            throws DeviceNotAvailableException {
+                        assertEquals("/data/0/com.android.shell/bugreports/*", deviceFilePath);
+                    }
+
+                    @Override
+                    public int getApiLevel() throws DeviceNotAvailableException {
+                        return 24;
+                    }
+                };
         FileInputStreamSource f = null;
         try {
             f = (FileInputStreamSource) mTestDevice.getBugreportz();
diff --git a/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java b/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
index e14cf50..4836cff 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
@@ -50,6 +50,7 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.util.Set;
 
 import javax.imageio.ImageIO;
 
@@ -819,4 +820,11 @@
         // restore initial value
         mTestDevice.setSetting(0, "system", "screen_brightness", initValue);
     }
+
+    /** Test for {@link TestDevice#listDisplayIds()}. */
+    @Test
+    public void testListDisplays() throws Exception {
+        Set<Integer> displays = mTestDevice.listDisplayIds();
+        assertEquals(1, displays.size());
+    }
 }
diff --git a/tests/src/com/android/tradefed/device/TestDeviceTest.java b/tests/src/com/android/tradefed/device/TestDeviceTest.java
index c26cb17..9561275 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceTest.java
@@ -31,6 +31,7 @@
 import com.android.tradefed.device.ITestDevice.ApexInfo;
 import com.android.tradefed.device.ITestDevice.MountPointInfo;
 import com.android.tradefed.device.ITestDevice.RecoveryMode;
+import com.android.tradefed.device.contentprovider.ContentProviderHandler;
 import com.android.tradefed.host.HostOptions;
 import com.android.tradefed.host.IHostOptions;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -4210,6 +4211,11 @@
                             throws DeviceNotAvailableException {
                         return mMockWifi;
                     }
+
+                    @Override
+                    ContentProviderHandler getContentProvider() throws DeviceNotAvailableException {
+                        return null;
+                    }
                 };
         mMockIDevice.executeShellCommand(
                 EasyMock.eq("dumpsys package com.android.tradefed.utils.wifi"),
@@ -4224,4 +4230,30 @@
         mTestDevice.postInvocationTearDown();
         verifyMocks();
     }
+
+    /** Test that displays can be collected. */
+    public void testListDisplayId() throws Exception {
+        CommandResult res = new CommandResult(CommandStatus.SUCCESS);
+        res.setStdout("Display 0 color modes:\nDisplay 5 color modes:\n");
+        EasyMock.expect(
+                        mMockRunUtil.runTimedCmd(
+                                100L,
+                                "adb",
+                                "-s",
+                                "serial",
+                                "shell",
+                                "dumpsys",
+                                "SurfaceFlinger",
+                                "|",
+                                "grep",
+                                "'color",
+                                "modes:'"))
+                .andReturn(res);
+        replayMocks();
+        Set<Integer> displays = mTestDevice.listDisplayIds();
+        assertEquals(2, displays.size());
+        assertTrue(displays.contains(0));
+        assertTrue(displays.contains(5));
+        verifyMocks();
+    }
 }
diff --git a/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
index 2c1395f..0ff4b1a 100644
--- a/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
+++ b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
@@ -55,12 +55,25 @@
         mProvider.tearDown();
     }
 
+    /** Test the install flow. */
     @Test
     public void testSetUp_install() throws Exception {
         Set<String> set = new HashSet<>();
         doReturn(set).when(mMockDevice).getInstalledPackageNames();
         doReturn(1).when(mMockDevice).getCurrentUser();
         doReturn(null).when(mMockDevice).installPackage(any(), eq(true), eq(true));
+        doReturn(null)
+                .when(mMockDevice)
+                .executeShellV2Command(
+                        String.format(
+                                "cmd appops set %s android:legacy_storage allow",
+                                ContentProviderHandler.PACKAGE_NAME));
+        CommandResult res = new CommandResult(CommandStatus.SUCCESS);
+        res.setStdout("LEGACY_STORAGE: allow");
+        doReturn(res)
+                .when(mMockDevice)
+                .executeShellV2Command(
+                        String.format("cmd appops get %s", ContentProviderHandler.PACKAGE_NAME));
 
         assertTrue(mProvider.setUp());
     }
diff --git a/tests/src/com/android/tradefed/device/metric/AtraceCollectorTest.java b/tests/src/com/android/tradefed/device/metric/AtraceCollectorTest.java
index 330743e..d5c4a5f 100644
--- a/tests/src/com/android/tradefed/device/metric/AtraceCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/AtraceCollectorTest.java
@@ -236,9 +236,7 @@
         EasyMock.expect(mMockDevice.pullFile(EasyMock.eq(M_DEFAULT_LOG_PATH)))
                 .andReturn(new File("/tmp/potato"))
                 .once();
-        EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("rm -f " + M_DEFAULT_LOG_PATH)))
-                .andReturn("")
-                .times(1);
+        mMockDevice.deleteFile(M_DEFAULT_LOG_PATH);
 
         EasyMock.replay(mMockDevice);
         mAtrace.onTestEnd(
diff --git a/tests/src/com/android/tradefed/device/metric/AtraceRunMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/AtraceRunMetricCollectorTest.java
index 6242789..c6a43a1 100644
--- a/tests/src/com/android/tradefed/device/metric/AtraceRunMetricCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/AtraceRunMetricCollectorTest.java
@@ -24,7 +24,6 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.util.FileUtil;
-import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -83,8 +82,6 @@
 
         OptionSetter setter = new OptionSetter(mAtraceRunMetricCollector);
         setter.setOptionValue("directory-keys", "sdcard/srcdirectory");
-        HashMap<String, Metric> currentMetrics = new HashMap<>();
-        currentMetrics.put("srcdirectory", TfMetricProtoUtil.stringToMetric("sdcard/srcdirectory"));
 
         Mockito.when(mMockDevice.pullDir(Mockito.eq("sdcard/srcdirectory"),
                 Mockito.any(File.class))).thenReturn(true);
diff --git a/tests/src/com/android/tradefed/suite/checker/UserCheckerTest.java b/tests/src/com/android/tradefed/suite/checker/UserCheckerTest.java
index 6c000ce..b013373 100644
--- a/tests/src/com/android/tradefed/suite/checker/UserCheckerTest.java
+++ b/tests/src/com/android/tradefed/suite/checker/UserCheckerTest.java
@@ -46,14 +46,14 @@
 
         ITestDevice preDevice =
                 mockDeviceUserState(
-                        /* users=        */ new Integer[] {0},
+                        /* userIds=        */ new Integer[] {0},
                         /* runningUsers= */ new Integer[] {0},
                         /* currentUser=  */ 0);
         assertEquals(CheckStatus.SUCCESS, checker.preExecutionCheck(preDevice).getStatus());
 
         ITestDevice postDevice =
                 mockDeviceUserState(
-                        /* users=        */ new Integer[] {0},
+                        /* userIds=        */ new Integer[] {0},
                         /* runningUsers= */ new Integer[] {0},
                         /* currentUser=  */ 0);
         assertEquals(CheckStatus.SUCCESS, checker.postExecutionCheck(postDevice).getStatus());
@@ -66,7 +66,7 @@
 
         ITestDevice preDevice =
                 mockDeviceUserState(
-                        /* users=        */ new Integer[] {0, 10, 11},
+                        /* userIds=        */ new Integer[] {0, 10, 11},
                         /* runningUsers= */ new Integer[] {0, 10},
                         /* currentUser=  */ 10);
         assertEquals(CheckStatus.SUCCESS, checker.preExecutionCheck(preDevice).getStatus());
@@ -74,7 +74,7 @@
         // User12 created, User11 deleted, User10 stopped, currentUser changed
         ITestDevice postDevice =
                 mockDeviceUserState(
-                        /* users=        */ new Integer[] {0, 10, 12},
+                        /* userIds=        */ new Integer[] {0, 10, 12},
                         /* runningUsers= */ new Integer[] {0},
                         /* currentUser=  */ 0);
         assertEquals(CheckStatus.FAILED, checker.postExecutionCheck(postDevice).getStatus());
@@ -87,7 +87,7 @@
         mOptionSetter.setOptionValue("user-type", "secondary");
         ITestDevice device =
                 mockDeviceUserState(
-                        /* users=        */ new Integer[] {0},
+                        /* userIds=        */ new Integer[] {0},
                         /* runningUsers= */ new Integer[] {0},
                         /* currentUser=  */ 0);
 
@@ -111,7 +111,7 @@
         mOptionSetter.setOptionValue("user-type", "secondary");
         ITestDevice device =
                 mockDeviceUserState(
-                        /* users=        */ new Integer[] {0},
+                        /* userIds=        */ new Integer[] {0},
                         /* runningUsers= */ new Integer[] {0},
                         /* currentUser=  */ 0);
 
@@ -128,12 +128,12 @@
     public void testFindRemovedUsers() throws Exception {
         DeviceUserState preState =
                 getMockedUserState(
-                        /* users=        */ new Integer[] {0, 10},
+                        /* userIds=        */ new Integer[] {0, 10},
                         /* runningUsers= */ new Integer[] {0, 10},
                         /* currentUser=  */ 0);
         DeviceUserState postState =
                 getMockedUserState(
-                        /* users=        */ new Integer[] {0},
+                        /* userIds=        */ new Integer[] {0},
                         /* runningUsers= */ new Integer[] {0},
                         /* currentUser=  */ 0);
 
@@ -144,12 +144,12 @@
     public void testFindAddedUsers() throws Exception {
         DeviceUserState preState =
                 getMockedUserState(
-                        /* users=        */ new Integer[] {0},
+                        /* userIds=        */ new Integer[] {0},
                         /* runningUsers= */ new Integer[] {0},
                         /* currentUser=  */ 0);
         DeviceUserState postState =
                 getMockedUserState(
-                        /* users=        */ new Integer[] {0, 10},
+                        /* userIds=        */ new Integer[] {0, 10},
                         /* runningUsers= */ new Integer[] {0},
                         /* currentUser=  */ 0);
 
@@ -160,12 +160,12 @@
     public void testCurrentUserChanged() throws Exception {
         DeviceUserState preState =
                 getMockedUserState(
-                        /* users=        */ new Integer[] {0, 10},
+                        /* userIds=        */ new Integer[] {0, 10},
                         /* runningUsers= */ new Integer[] {0, 10},
                         /* currentUser=  */ 10);
         DeviceUserState postState =
                 getMockedUserState(
-                        /* users=        */ new Integer[] {0, 10},
+                        /* userIds=        */ new Integer[] {0, 10},
                         /* runningUsers= */ new Integer[] {0, 10},
                         /* currentUser=  */ 0);
 
@@ -176,12 +176,12 @@
     public void testfindStartedUsers() throws Exception {
         DeviceUserState preState =
                 getMockedUserState(
-                        /* users=        */ new Integer[] {0, 10},
+                        /* userIds=        */ new Integer[] {0, 10},
                         /* runningUsers= */ new Integer[] {0},
                         /* currentUser=  */ 0);
         DeviceUserState postState =
                 getMockedUserState(
-                        /* users=        */ new Integer[] {0, 10},
+                        /* userIds=        */ new Integer[] {0, 10},
                         /* runningUsers= */ new Integer[] {0, 10},
                         /* currentUser=  */ 0);
 
@@ -193,12 +193,12 @@
     public void testFindStopedUsers() throws Exception {
         DeviceUserState preState =
                 getMockedUserState(
-                        /* users=        */ new Integer[] {0, 10},
+                        /* userIds=        */ new Integer[] {0, 10},
                         /* runningUsers= */ new Integer[] {0, 10},
                         /* currentUser=  */ 0);
         DeviceUserState postState =
                 getMockedUserState(
-                        /* users=        */ new Integer[] {0, 10},
+                        /* userIds=        */ new Integer[] {0, 10},
                         /* runningUsers= */ new Integer[] {0},
                         /* currentUser=  */ 0);
 
diff --git a/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java b/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
index 745175f..a7426a8 100644
--- a/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
+++ b/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
@@ -1079,9 +1079,7 @@
         doSetupExpectations();
         doCheckExternalStoreSpaceExpectations();
         EasyMock.expect(mMockDevice.pullFile("/data/local.prop")).andReturn(null).once();
-        EasyMock.expect(mMockDevice.executeShellCommand("rm -f /data/local.prop"))
-                .andReturn(null)
-                .once();
+        mMockDevice.deleteFile("/data/local.prop");
         mMockDevice.reboot();
         EasyMock.expectLastCall().once();
 
diff --git a/tests/src/com/android/tradefed/testtype/AndroidJUnitTestTest.java b/tests/src/com/android/tradefed/testtype/AndroidJUnitTestTest.java
index 7e61ffd..11ea3cb 100644
--- a/tests/src/com/android/tradefed/testtype/AndroidJUnitTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/AndroidJUnitTestTest.java
@@ -223,7 +223,8 @@
                 EasyMock.<File>anyObject(), EasyMock.<String>anyObject())).andReturn(Boolean.TRUE);
         EasyMock.expect(mMockTestDevice.executeShellCommand(EasyMock.<String>anyObject()))
                 .andReturn("")
-                .times(2);
+                .times(1);
+        mMockTestDevice.deleteFile("/data/local/tmp/ajur");
         EasyMock.replay(mMockRemoteRunner, mMockTestDevice);
 
         File tmpFile = FileUtil.createTempFile("testFile", ".txt");
@@ -248,7 +249,8 @@
                 EasyMock.<File>anyObject(), EasyMock.<String>anyObject())).andReturn(Boolean.TRUE);
         EasyMock.expect(mMockTestDevice.executeShellCommand(EasyMock.<String>anyObject()))
                 .andReturn("")
-                .times(2);
+                .times(1);
+        mMockTestDevice.deleteFile("/data/local/tmp/ajur");
         EasyMock.replay(mMockRemoteRunner, mMockTestDevice);
 
         File tmpFile = FileUtil.createTempFile("notTestFile", ".txt");
@@ -279,7 +281,8 @@
                 EasyMock.<String>anyObject())).andReturn(Boolean.TRUE).times(2);
         EasyMock.expect(mMockTestDevice.executeShellCommand(EasyMock.<String>anyObject()))
                 .andReturn("")
-                .times(3);
+                .times(2);
+        mMockTestDevice.deleteFile("/data/local/tmp/ajur");
         EasyMock.replay(mMockRemoteRunner, mMockTestDevice);
 
         File tmpFileInclude = FileUtil.createTempFile("includeFile", ".txt");
@@ -351,7 +354,8 @@
                 .times(2);
         EasyMock.expect(mMockTestDevice.executeShellCommand(EasyMock.<String>anyObject()))
                 .andReturn("")
-                .times(3);
+                .times(2);
+        mMockTestDevice.deleteFile("/data/local/tmp/ajur");
         EasyMock.replay(mMockRemoteRunner, mMockTestDevice);
 
         File tmpFileInclude = FileUtil.createTempFile("includeFile", ".txt");
diff --git a/tests/src/com/android/tradefed/testtype/DeviceJUnit4ClassRunnerTest.java b/tests/src/com/android/tradefed/testtype/DeviceJUnit4ClassRunnerTest.java
new file mode 100644
index 0000000..82ef7c9
--- /dev/null
+++ b/tests/src/com/android/tradefed/testtype/DeviceJUnit4ClassRunnerTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype;
+
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.doReturn;
+
+import com.android.tradefed.build.BuildInfo;
+import com.android.tradefed.config.ConfigurationDef;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.DynamicRemoteFileResolver;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.config.remote.GcsRemoteFileResolver;
+import com.android.tradefed.config.remote.IRemoteFileResolver;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.TestDescription;
+
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.runners.model.InitializationError;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.util.HashMap;
+
+/** Unit tests for {@link DeviceJUnit4ClassRunner}. */
+@RunWith(JUnit4.class)
+public class DeviceJUnit4ClassRunnerTest {
+
+    private static final File FAKE_REMOTE_FILE_PATH = new File("gs://bucket/test/file.txt");
+
+    /** Class that allow testing. */
+    public static class TestableRunner extends DeviceJUnit4ClassRunner {
+
+        public TestableRunner(Class<?> klass) throws InitializationError {
+            super(klass);
+        }
+
+        @Override
+        OptionSetter createOptionSetter(Object obj) throws ConfigurationException {
+            return new OptionSetter(obj) {
+                @Override
+                protected DynamicRemoteFileResolver createResolver() {
+                    DynamicRemoteFileResolver mResolver =
+                            new DynamicRemoteFileResolver() {
+                                @Override
+                                protected IRemoteFileResolver getResolver(String protocol) {
+                                    if (protocol.equals(GcsRemoteFileResolver.PROTOCOL)) {
+                                        IRemoteFileResolver mockResolver =
+                                                Mockito.mock(IRemoteFileResolver.class);
+                                        try {
+                                            doReturn(new File("/downloaded/somewhere"))
+                                                    .when(mockResolver)
+                                                    .resolveRemoteFiles(
+                                                            Mockito.eq(FAKE_REMOTE_FILE_PATH),
+                                                            Mockito.any());
+                                            return mockResolver;
+                                        } catch (ConfigurationException e) {
+                                            CLog.e(e);
+                                        }
+                                    }
+                                    return null;
+                                }
+
+                                @Override
+                                protected boolean updateProtocols() {
+                                    // Do not set the static variable
+                                    return false;
+                                }
+                            };
+                    return mResolver;
+                }
+            };
+        }
+    }
+
+    @RunWith(TestableRunner.class)
+    public static class Junit4TestClass {
+
+        public Junit4TestClass() {}
+
+        @Option(name = "dynamic-option")
+        public File mOption = FAKE_REMOTE_FILE_PATH;
+
+        @Test
+        public void testPass() {
+            assertNotNull(mOption);
+            assertNotEquals(FAKE_REMOTE_FILE_PATH, mOption);
+        }
+    }
+
+    private ITestInvocationListener mListener;
+    private HostTest mHostTest;
+    private ITestDevice mMockDevice;
+
+    @Before
+    public void setUp() throws Exception {
+        mListener = EasyMock.createMock(ITestInvocationListener.class);
+        mMockDevice = EasyMock.createMock(ITestDevice.class);
+        mHostTest = new HostTest();
+        mHostTest.setBuild(new BuildInfo());
+        mHostTest.setDevice(mMockDevice);
+        IInvocationContext context = new InvocationContext();
+        context.addAllocatedDevice(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockDevice);
+        OptionSetter setter = new OptionSetter(mHostTest);
+        // Disable pretty logging for testing
+        setter.setOptionValue("enable-pretty-logs", "false");
+    }
+
+    @Test
+    public void testDynamicDownload() throws Exception {
+        mHostTest.setClassName(Junit4TestClass.class.getName());
+        TestDescription test1 = new TestDescription(Junit4TestClass.class.getName(), "testPass");
+        mListener.testRunStarted((String) EasyMock.anyObject(), EasyMock.eq(1));
+        mListener.testStarted(EasyMock.eq(test1));
+        mListener.testEnded(EasyMock.eq(test1), EasyMock.<HashMap<String, Metric>>anyObject());
+        mListener.testRunEnded(EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
+        EasyMock.replay(mListener);
+        mHostTest.run(mListener);
+        EasyMock.verify(mListener);
+    }
+}
diff --git a/tests/src/com/android/tradefed/testtype/GTestTest.java b/tests/src/com/android/tradefed/testtype/GTestTest.java
index d85e193..c8441a6 100644
--- a/tests/src/com/android/tradefed/testtype/GTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestTest.java
@@ -312,8 +312,7 @@
                 EasyMock.same(mMockReceiver), EasyMock.anyLong(), (TimeUnit)EasyMock.anyObject(),
                 EasyMock.anyInt());
         // Expect deletion of file on device
-        EasyMock.expect(mMockITestDevice.executeShellCommand(
-                EasyMock.eq(String.format("rm %s", deviceScriptPath)))).andReturn("");
+        mMockITestDevice.deleteFile(deviceScriptPath);
         replayMocks();
         mGTest.run(mMockInvocationListener);
 
diff --git a/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java b/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
index f0c0acd..a267e95 100644
--- a/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
@@ -22,6 +22,8 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import com.android.tradefed.build.DeviceBuildInfo;
+import com.android.tradefed.build.IDeviceBuildInfo;
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
@@ -75,7 +77,7 @@
     public void testRunHostExecutable_doesNotExists() throws Exception {
         String path = "/does/not/exists/path/bin/test";
         OptionSetter setter = new OptionSetter(mExecutableTest);
-        setter.setOptionValue("binary-path", path);
+        setter.setOptionValue("binary", path);
 
         mExecutableTest.run(mMockListener);
 
@@ -91,7 +93,7 @@
         File tmpBinary = FileUtil.createTempFile("test-executable", "");
         try {
             OptionSetter setter = new OptionSetter(mExecutableTest);
-            setter.setOptionValue("binary-path", tmpBinary.getAbsolutePath());
+            setter.setOptionValue("binary", tmpBinary.getAbsolutePath());
 
             CommandResult result = new CommandResult(CommandStatus.SUCCESS);
             doReturn(result)
@@ -109,12 +111,73 @@
         }
     }
 
+    /** If the binary is available from the tests directory we can find it and run it. */
+    @Test
+    public void testRunHostExecutable_search() throws Exception {
+        File testsDir = FileUtil.createTempDir("executable-tests-dir");
+        File tmpBinary = FileUtil.createTempFile("test-executable", "", testsDir);
+        try {
+            IDeviceBuildInfo info = new DeviceBuildInfo();
+            info.setTestsDir(testsDir, "testversion");
+            mExecutableTest.setBuild(info);
+            OptionSetter setter = new OptionSetter(mExecutableTest);
+            setter.setOptionValue("binary", tmpBinary.getName());
+
+            CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+            doReturn(result)
+                    .when(mMockRunUtil)
+                    .runTimedCmd(Mockito.anyLong(), Mockito.eq(tmpBinary.getAbsolutePath()));
+
+            mExecutableTest.run(mMockListener);
+
+            verify(mMockListener, Mockito.times(1)).testRunStarted(eq(tmpBinary.getName()), eq(1));
+            verify(mMockListener, Mockito.times(0)).testRunFailed(any());
+            verify(mMockListener, Mockito.times(1))
+                    .testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
+        } finally {
+            FileUtil.recursiveDelete(testsDir);
+        }
+    }
+
+    @Test
+    public void testRunHostExecutable_notFound() throws Exception {
+        File testsDir = FileUtil.createTempDir("executable-tests-dir");
+        File tmpBinary = FileUtil.createTempFile("test-executable", "", testsDir);
+        try {
+            IDeviceBuildInfo info = new DeviceBuildInfo();
+            info.setTestsDir(testsDir, "testversion");
+            mExecutableTest.setBuild(info);
+            OptionSetter setter = new OptionSetter(mExecutableTest);
+            setter.setOptionValue("binary", tmpBinary.getName());
+            tmpBinary.delete();
+
+            CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+            doReturn(result)
+                    .when(mMockRunUtil)
+                    .runTimedCmd(Mockito.anyLong(), Mockito.eq(tmpBinary.getAbsolutePath()));
+
+            mExecutableTest.run(mMockListener);
+
+            verify(mMockListener, Mockito.times(1)).testRunStarted(eq(tmpBinary.getName()), eq(0));
+            verify(mMockListener, Mockito.times(1))
+                    .testRunFailed(
+                            eq(
+                                    String.format(
+                                            ExecutableBaseTest.NO_BINARY_ERROR,
+                                            tmpBinary.getName())));
+            verify(mMockListener, Mockito.times(1))
+                    .testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
+        } finally {
+            FileUtil.recursiveDelete(testsDir);
+        }
+    }
+
     @Test
     public void testRunHostExecutable_failure() throws Exception {
         File tmpBinary = FileUtil.createTempFile("test-executable", "");
         try {
             OptionSetter setter = new OptionSetter(mExecutableTest);
-            setter.setOptionValue("binary-path", tmpBinary.getAbsolutePath());
+            setter.setOptionValue("binary", tmpBinary.getAbsolutePath());
 
             CommandResult result = new CommandResult(CommandStatus.FAILED);
             result.setExitCode(5);
diff --git a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
index 3ae5415..8e51bc8 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
@@ -1502,9 +1502,6 @@
     public void testNoAbi() throws Exception {
         EasyMock.reset(mMockDevice);
         EasyMock.expect(mMockDevice.getIDevice()).andStubReturn(new TcpDevice("tcp-device-0"));
-        Set<String> expectedAbis = new HashSet<>();
-        expectedAbis.add("arm64-v8a");
-        expectedAbis.add("armeabi-v7a");
 
         EasyMock.expect(mMockDevice.getProperty("ro.product.cpu.abilist")).andReturn(null);
         EasyMock.expect(mMockDevice.getProperty("ro.product.cpu.abi")).andReturn(null);
diff --git a/tests/src/com/android/tradefed/util/testmapping/TestInfoTest.java b/tests/src/com/android/tradefed/util/testmapping/TestInfoTest.java
index cd06932..dbc97a5 100644
--- a/tests/src/com/android/tradefed/util/testmapping/TestInfoTest.java
+++ b/tests/src/com/android/tradefed/util/testmapping/TestInfoTest.java
@@ -43,7 +43,11 @@
         assertEquals("option2", info.getOptions().get(1).getName());
         assertEquals("value2", info.getOptions().get(1).getValue());
         assertEquals(
-                "test1; Options: option1:value1,option2:value2; Keywords: key1,key2",
+                "test1\n\t" +
+                "Options: option1:value1,option2:value2\n\t" +
+                "Keywords: key1,key2\n\t" +
+                "Sources: folder1\n\t" +
+                "Host: false",
                 info.toString());
         assertEquals("test1 - false", info.getNameAndHostOnly());
     }
diff --git a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
index efc5ce8..56cfa8c 100644
--- a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
+++ b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
@@ -32,10 +32,12 @@
 
 import java.io.File;
 import java.io.InputStream;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /** Unit tests for {@link TestMapping}. */
@@ -61,35 +63,56 @@
             String rootDirName = testMappingRootDir.getName();
             testMappingFile =
                     FileUtil.saveResourceFile(resourceStream, testMappingRootDir, TEST_MAPPING);
-            List<TestInfo> tests =
+            Set<TestInfo> tests =
                     new TestMapping(testMappingFile.toPath(), Paths.get(tempDir.getAbsolutePath()))
                             .getTests("presubmit", null, true, null);
             assertEquals(1, tests.size());
-            assertEquals("test1", tests.get(0).getName());
-
+            Set<String> names = new HashSet<String>();
+            for (TestInfo test : tests) {
+                names.add(test.getName());
+            }
+            assertTrue(names.contains("test1"));
             tests =
                     new TestMapping(testMappingFile.toPath(), Paths.get(tempDir.getAbsolutePath()))
                             .getTests("presubmit", null, false, null);
             assertEquals(1, tests.size());
-            assertEquals("suite/stub1", tests.get(0).getName());
-
+            names = new HashSet<String>();
+            for (TestInfo test : tests) {
+                names.add(test.getName());
+            }
+            assertTrue(names.contains("suite/stub1"));
             tests =
                     new TestMapping(testMappingFile.toPath(), Paths.get(tempDir.getAbsolutePath()))
                             .getTests("postsubmit", null, false, null);
             assertEquals(2, tests.size());
-            assertEquals("test2", tests.get(0).getName());
-            TestOption option = tests.get(0).getOptions().get(0);
-            assertEquals("instrumentation-arg", option.getName());
-            assertEquals(
-                    "annotation=android.platform.test.annotations.Presubmit", option.getValue());
-            assertEquals("instrument", tests.get(1).getName());
+            TestOption testOption =
+                    new TestOption(
+                            "instrumentation-arg",
+                            "annotation=android.platform.test.annotations.Presubmit");
+            names = new HashSet<String>();
+            Set<TestOption> testOptions = new HashSet<TestOption>();
+            for (TestInfo test : tests) {
+                names.add(test.getName());
+                testOptions.addAll(test.getOptions());
+            }
+            assertTrue(names.contains("test2"));
+            assertTrue(names.contains("instrument"));
+            assertTrue(testOptions.contains(testOption));
             tests =
                     new TestMapping(testMappingFile.toPath(), Paths.get(tempDir.getAbsolutePath()))
                             .getTests("othertype", null, false, null);
             assertEquals(1, tests.size());
-            assertEquals("test3", tests.get(0).getName());
-            assertEquals(1, tests.get(0).getSources().size());
-            assertTrue(tests.get(0).getSources().contains(rootDirName));
+            names = new HashSet<String>();
+            testOptions = new HashSet<TestOption>();
+            Set<String> sources = new HashSet<String>();
+            for (TestInfo test : tests) {
+                names.add(test.getName());
+                testOptions.addAll(test.getOptions());
+                sources.addAll(test.getSources());
+            }
+            assertTrue(names.contains("test3"));
+            assertEquals(1, testOptions.size());
+            assertTrue(sources.contains(rootDirName));
         } finally {
             FileUtil.recursiveDelete(tempDir);
         }
@@ -104,7 +127,7 @@
             tempDir = FileUtil.createTempDir("test_mapping");
             File testMappingFile = Paths.get(tempDir.getAbsolutePath(), TEST_MAPPING).toFile();
             FileUtil.writeToFile("bad format json file", testMappingFile);
-            List<TestInfo> tests =
+            Set<TestInfo> tests =
                     new TestMapping(testMappingFile.toPath(), Paths.get(tempDir.getAbsolutePath()))
                             .getTests("presubmit", null, false, null);
         } finally {
@@ -374,4 +397,111 @@
         assertTrue(new HashSet<TestOption>(test1.getOptions()).contains(optionExcludeAnnotation2));
         assertTrue(new HashSet<TestOption>(test1.getOptions()).contains(option2));
     }
+
+    /** Test for {@link TestMapping#getAllTests()} for loading tests from test_mappings directory. */
+    @Test
+    public void testGetAllTests() 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 + "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);
+            srcFile = File.separator + TEST_DATA_DIR + File.separator + DISABLED_PRESUBMIT_TESTS;
+            resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, tempDir, DISABLED_PRESUBMIT_TESTS);
+
+            Map<String, Set<TestInfo>> allTests = TestMapping.getAllTests(tempDir);
+            Set<TestInfo> tests = allTests.get("presubmit");
+            assertEquals(5, tests.size());
+
+            tests = allTests.get("postsubmit");
+            assertEquals(4, tests.size());
+
+            tests = allTests.get("othertype");
+            assertEquals(1, tests.size());
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    /** Test for {@link TestMapping#extractTestMappingsZip()} for extracting test mappings zip. */
+    @Test
+    public void testExtractTestMappingsZip() throws Exception {
+        File tempDir = null;
+        File extractedFile = null;
+        try {
+            tempDir = FileUtil.createTempDir("test_mapping");
+
+            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);
+            srcFile = File.separator + TEST_DATA_DIR + File.separator + DISABLED_PRESUBMIT_TESTS;
+            resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, tempDir, DISABLED_PRESUBMIT_TESTS);
+            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);
+
+            extractedFile = TestMapping.extractTestMappingsZip(zipFile);
+            Map<String, Set<TestInfo>> allTests = TestMapping.getAllTests(tempDir);
+            Set<TestInfo> tests = allTests.get("presubmit");
+            assertEquals(5, tests.size());
+
+            tests = allTests.get("postsubmit");
+            assertEquals(4, tests.size());
+
+            tests = allTests.get("othertype");
+            assertEquals(1, tests.size());
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+            FileUtil.recursiveDelete(extractedFile);
+        }
+    }
+
+    /** Test for {@link TestMapping#extractTestMappingsZip()} for extracting test mappings zip. */
+    @Test
+    public void testGetDisabledTests() 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 + "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);
+            srcFile = File.separator + TEST_DATA_DIR + File.separator + DISABLED_PRESUBMIT_TESTS;
+            resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, tempDir, DISABLED_PRESUBMIT_TESTS);
+            Path tempDirPath = Paths.get(tempDir.getAbsolutePath());
+            Set<String> disabledTests = TestMapping.getDisabledTests(tempDirPath, "presubmit");
+            assertEquals(2, disabledTests.size());
+
+            disabledTests = TestMapping.getDisabledTests(tempDirPath, "postsubmit");
+            assertEquals(0, disabledTests.size());
+
+            disabledTests = TestMapping.getDisabledTests(tempDirPath, "othertype");
+            assertEquals(0, disabledTests.size());
+
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
 }