Merge "GoogleBenchmarkTest: Add "benchmark-filter" option."
diff --git a/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java b/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
index 0d10607..e3d7de1 100644
--- a/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
+++ b/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
@@ -176,11 +176,6 @@
     private File mAvdConfigFile = null;
 
     @Option(
-            name = "gce-driver-config-test-resource-name",
-            description = "Test resource name of the config to use to launch GCE devices.")
-    private String mAvdConfigTestResourceName;
-
-    @Option(
             name = "gce-driver-service-account-json-key-path",
             description = "path to the service account json key location.")
     private File mJsonKeyFile = null;
@@ -549,11 +544,6 @@
         mAvdConfigFile = avdConfigFile;
     }
 
-    /** Return the Gce Avd config test resource name to start the instance. */
-    public String getAvdConfigTestResourceName() {
-        return mAvdConfigTestResourceName;
-    }
-
     /** @return the service account json key file. */
     public File getServiceAccountJsonKeyFile() {
         return mJsonKeyFile;
diff --git a/src/com/android/tradefed/build/BuildInfo.java b/src/com/android/tradefed/build/BuildInfo.java
index 4d42ea0..2069b2a 100644
--- a/src/com/android/tradefed/build/BuildInfo.java
+++ b/src/com/android/tradefed/build/BuildInfo.java
@@ -215,7 +215,6 @@
         setBuildFlavor(build.getBuildFlavor());
         setBuildBranch(build.getBuildBranch());
         setTestTag(build.getTestTag());
-        setTestResourceBuild(build.isTestResourceBuild());
     }
 
     protected MultiMap<String, String> getAttributesMultiMap() {
@@ -681,32 +680,9 @@
                         buildFile.getVersion());
             }
         }
-        // Test resource
-        buildInfo.setTestResourceBuild(protoBuild.getIsTestResource());
         return buildInfo;
     }
 
-    /**
-     * Get test resource from a list of builds.
-     *
-     * @param testResourceBuildInfos An list of {@link IBuildInfo}.
-     * @param testResourceName the test resource name
-     * @return the test resource file.
-     */
-    public static File getTestResource(
-            List<IBuildInfo> testResourceBuildInfos, String testResourceName) {
-        if (testResourceBuildInfos == null) {
-            return null;
-        }
-        for (IBuildInfo buildInfo : testResourceBuildInfos) {
-            File testResourceFile = buildInfo.getFile(testResourceName);
-            if (testResourceFile != null) {
-                return testResourceFile;
-            }
-        }
-        return null;
-    }
-
     /** {@inheritDoc} */
     @Override
     public Set<File> getRemoteFiles() {
diff --git a/src/com/android/tradefed/build/GCSTestResourceProvider.java b/src/com/android/tradefed/build/GCSTestResourceProvider.java
deleted file mode 100644
index aabbeb2..0000000
--- a/src/com/android/tradefed/build/GCSTestResourceProvider.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.build;
-
-import com.android.tradefed.build.gcs.GCSDownloaderHelper;
-import com.android.tradefed.config.Option;
-
-import com.google.common.annotations.VisibleForTesting;
-
-import java.io.File;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Download test resource from GCS. */
-public class GCSTestResourceProvider implements IBuildProvider {
-
-    @Option(
-            name = "test-resource",
-            description =
-                    "GCS files as test resources that are required for the test."
-                            + "Key is the identity of the test resource."
-                            + "Value is a gs://bucket/path/to/file format GCS path.")
-    private Map<String, String> mTestResources = new HashMap<>();
-
-    private IBuildInfo mBuildInfo;
-    private GCSDownloaderHelper mDownloaderHelper = null;
-
-    @Override
-    public IBuildInfo getBuild() throws BuildRetrievalError {
-        mBuildInfo = new BuildInfo();
-        mBuildInfo.setTestResourceBuild(true);
-        fetchTestResources();
-        return mBuildInfo;
-    }
-
-    private void fetchTestResources() throws BuildRetrievalError {
-        for (Map.Entry<String, String> entry : mTestResources.entrySet()) {
-            fetchTestResource(entry.getKey(), entry.getValue());
-        }
-    }
-
-    private void fetchTestResource(String key, String value) throws BuildRetrievalError {
-        File localFile = getHelper().fetchTestResource(value);
-        mBuildInfo.setFile(key, localFile, "");
-    }
-
-    @Override
-    public void cleanUp(IBuildInfo info) {
-        info.cleanUp();
-    }
-
-    /** Returns the {@link GCSDownloaderHelper} that downloads from GCS buckets. */
-    @VisibleForTesting
-    GCSDownloaderHelper getHelper() {
-        if (mDownloaderHelper == null) {
-            mDownloaderHelper = new GCSDownloaderHelper();
-        }
-        return mDownloaderHelper;
-    }
-}
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index fd00a3c..cc5b8d6 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -447,8 +447,7 @@
         String propValue = getProperty(propName);
         if (propValue != null) {
             return propValue;
-        } else if (TestDeviceState.FASTBOOT.equals(getDeviceState()) &&
-                fastbootVar != null) {
+        } else if (isStateBootloaderOrFastbootd() && fastbootVar != null) {
             CLog.i("%s for device %s is null, re-querying in fastboot", description,
                     getSerialNumber());
             return getFastbootVariable(fastbootVar);
@@ -2936,11 +2935,11 @@
             throws DeviceNotAvailableException, UnsupportedOperationException {
         rebootIntoFastbootInternal(false);
     }
-    
+
     /**
      * Reboots the device into bootloader or fastbootd mode.
      *
-     * @param isBootloader: true to boot the device into bootloader mode, false to boot the device
+     * @param isBootloader true to boot the device into bootloader mode, false to boot the device
      *     into fastbootd mode.
      * @throws DeviceNotAvailableException if connection with device is lost and cannot be
      *     recovered.
@@ -2948,7 +2947,7 @@
     private void rebootIntoFastbootInternal(boolean isBootloader)
             throws DeviceNotAvailableException {
         final RebootMode mode =
-                isBootloader ? RebootMode.REBOOT_INTO_BOOTLOADER : RebootMode.REBOOT_INTO_FASTBOOT;
+                isBootloader ? RebootMode.REBOOT_INTO_BOOTLOADER : RebootMode.REBOOT_INTO_FASTBOOTD;
         if (!mFastbootEnabled) {
             throw new UnsupportedOperationException(
                     String.format("Fastboot is not available and cannot reboot into %s", mode));
@@ -2958,13 +2957,14 @@
         CLog.i(
                 "Rebooting device %s in state %s into %s",
                 getSerialNumber(), getDeviceState(), mode);
-        if (TestDeviceState.FASTBOOT.equals(getDeviceState())) {
+        if (isStateBootloaderOrFastbootd()) {
             CLog.i("device %s already in fastboot. Rebooting anyway", getSerialNumber());
             executeFastbootCommand(String.format("reboot-%s", mode));
         } else {
             CLog.i("Booting device %s into %s", getSerialNumber(), mode);
             doAdbReboot(mode, null);
         }
+
         if (!mStateMonitor.waitForDeviceBootloader(mOptions.getFastbootTimeout())) {
             recoverDeviceFromBootloader();
         }
@@ -3120,7 +3120,7 @@
     protected enum RebootMode {
         REBOOT_FULL(""),
         REBOOT_USERSPACE("userspace"),
-        REBOOT_INTO_FASTBOOT("fastboot"),
+        REBOOT_INTO_FASTBOOTD("fastboot"),
         REBOOT_INTO_BOOTLOADER("bootloader"),
         REBOOT_INTO_SIDELOAD("sideload"),
         REBOOT_INTO_SIDELOAD_AUTO_REBOOT("sideload-auto-reboot"),
@@ -3161,8 +3161,8 @@
         // Track Tradefed reboot time
         mLastTradefedRebootTime = System.currentTimeMillis();
 
-        if (TestDeviceState.FASTBOOT == getDeviceState()) {
-            CLog.i("device %s in fastboot. Rebooting to userspace.", getSerialNumber());
+        if (isStateBootloaderOrFastbootd()) {
+            CLog.i("device %s in %s. Rebooting to userspace.", getSerialNumber(), getDeviceState());
             executeFastbootCommand("reboot");
         } else {
             if (mOptions.shouldDisableReboot()) {
@@ -3730,7 +3730,7 @@
         if (!deviceState.equals(getDeviceState())) {
             // disable state changes while fastboot lock is held, because issuing fastboot command
             // will disrupt state
-            if (getDeviceState().equals(TestDeviceState.FASTBOOT) && mFastbootLock.isLocked()) {
+            if (isStateBootloaderOrFastbootd() && mFastbootLock.isLocked()) {
                 return;
             }
             mState = deviceState;
@@ -4869,7 +4869,7 @@
         if (getIDevice() instanceof StubDevice) {
             return null;
         }
-        if (TestDeviceState.FASTBOOT.equals(getDeviceState())) {
+        if (isStateBootloaderOrFastbootd()) {
             return null;
         }
         try {
diff --git a/src/com/android/tradefed/device/WaitDeviceRecovery.java b/src/com/android/tradefed/device/WaitDeviceRecovery.java
index 66d25c3..e163025 100644
--- a/src/com/android/tradefed/device/WaitDeviceRecovery.java
+++ b/src/com/android/tradefed/device/WaitDeviceRecovery.java
@@ -120,10 +120,13 @@
         // ensure bootloader state is updated
         monitor.waitForDeviceBootloaderStateUpdate();
 
-        if (monitor.getDeviceState().equals(TestDeviceState.FASTBOOT)) {
-            Log.i(LOG_TAG, String.format(
-                    "Found device %s in fastboot but expected online. Rebooting...",
-                    monitor.getSerialNumber()));
+        TestDeviceState state = monitor.getDeviceState();
+        if (TestDeviceState.FASTBOOT.equals(state) || TestDeviceState.FASTBOOTD.equals(state)) {
+            Log.i(
+                    LOG_TAG,
+                    String.format(
+                            "Found device %s in %s but expected online. Rebooting...",
+                            monitor.getSerialNumber(), state));
             // TODO: retry if failed
             getRunUtil().runTimedCmd(mFastbootWaitTime, mFastbootPath, "-s",
                     monitor.getSerialNumber(), "reboot");
diff --git a/src/com/android/tradefed/device/cloud/GceManager.java b/src/com/android/tradefed/device/cloud/GceManager.java
index 3e6a77d..4ef0434 100644
--- a/src/com/android/tradefed/device/cloud/GceManager.java
+++ b/src/com/android/tradefed/device/cloud/GceManager.java
@@ -15,7 +15,6 @@
  */
 package com.android.tradefed.device.cloud;
 
-import com.android.tradefed.build.BuildInfo;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.command.remote.DeviceDescriptor;
 import com.android.tradefed.device.TestDeviceOptions;
@@ -71,7 +70,6 @@
     private DeviceDescriptor mDeviceDescriptor;
     private TestDeviceOptions mDeviceOptions;
     private IBuildInfo mBuildInfo;
-    private List<IBuildInfo> mTestResourceBuildInfos;
 
     private String mGceInstanceName = null;
     private String mGceHost = null;
@@ -83,17 +81,12 @@
      * @param deviceDesc The {@link DeviceDescriptor} that will be associated with the GCE device.
      * @param deviceOptions A {@link TestDeviceOptions} associated with the device.
      * @param buildInfo A {@link IBuildInfo} describing the gce build to start.
-     * @param testResourceBuildInfos A list {@link IBuildInfo} describing test resources
      */
     public GceManager(
-            DeviceDescriptor deviceDesc,
-            TestDeviceOptions deviceOptions,
-            IBuildInfo buildInfo,
-            List<IBuildInfo> testResourceBuildInfos) {
+            DeviceDescriptor deviceDesc, TestDeviceOptions deviceOptions, IBuildInfo buildInfo) {
         mDeviceDescriptor = deviceDesc;
         mDeviceOptions = deviceOptions;
         mBuildInfo = buildInfo;
-        mTestResourceBuildInfos = testResourceBuildInfos;
 
         if (!deviceOptions.allowGceCmdTimeoutOverride()) {
             return;
@@ -117,13 +110,22 @@
         }
     }
 
+    /** @deprecated Use other constructors, we keep this temporarily for backward compatibility. */
+    @Deprecated
+    public GceManager(
+            DeviceDescriptor deviceDesc,
+            TestDeviceOptions deviceOptions,
+            IBuildInfo buildInfo,
+            List<IBuildInfo> testResourceBuildInfos) {
+        this(deviceDesc, deviceOptions, buildInfo);
+    }
+
     /**
      * Ctor, variation that can be used to provide the GCE instance name to use directly.
      *
      * @param deviceDesc The {@link DeviceDescriptor} that will be associated with the GCE device.
      * @param deviceOptions A {@link TestDeviceOptions} associated with the device
      * @param buildInfo A {@link IBuildInfo} describing the gce build to start.
-     * @param testResourceBuildInfos A list {@link IBuildInfo} describing test resources
      * @param gceInstanceName The instance name to use.
      * @param gceHost The host name or ip of the instance to use.
      */
@@ -131,10 +133,9 @@
             DeviceDescriptor deviceDesc,
             TestDeviceOptions deviceOptions,
             IBuildInfo buildInfo,
-            List<IBuildInfo> testResourceBuildInfos,
             String gceInstanceName,
             String gceHost) {
-        this(deviceDesc, deviceOptions, buildInfo, testResourceBuildInfos);
+        this(deviceDesc, deviceOptions, buildInfo);
         mGceInstanceName = gceInstanceName;
         mGceHost = gceHost;
     }
@@ -729,10 +730,6 @@
 
     @VisibleForTesting
     File getAvdConfigFile() {
-        if (getTestDeviceOptions().getAvdConfigTestResourceName() != null) {
-            return BuildInfo.getTestResource(
-                    mTestResourceBuildInfos, getTestDeviceOptions().getAvdConfigTestResourceName());
-        }
         return getTestDeviceOptions().getAvdConfigFile();
     }
 
diff --git a/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java b/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java
index f196cfe..282c9bd 100644
--- a/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java
+++ b/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java
@@ -79,7 +79,7 @@
         // First get the options
         TestDeviceOptions options = getOptions();
         // We create a brand new GceManager each time to ensure clean state.
-        mGceHandler = new GceManager(getDeviceDescriptor(), options, info, testResourceBuildInfos);
+        mGceHandler = new GceManager(getDeviceDescriptor(), options, info);
         getGceHandler().logStableHostImageInfos(info);
         setFastbootEnabled(false);
 
diff --git a/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java b/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
index d0c2875..641fe31 100644
--- a/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
+++ b/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
@@ -91,9 +91,7 @@
             mGceAvd = null;
             mGceSshMonitor = null;
             // We create a brand new GceManager each time to ensure clean state.
-            mGceHandler =
-                    new GceManager(
-                            getDeviceDescriptor(), getOptions(), info, testResourceBuildInfos);
+            mGceHandler = new GceManager(getDeviceDescriptor(), getOptions(), info);
             getGceHandler().logStableHostImageInfos(info);
             setFastbootEnabled(false);
 
diff --git a/src/com/android/tradefed/device/metric/FilePullerDeviceMetricCollector.java b/src/com/android/tradefed/device/metric/FilePullerDeviceMetricCollector.java
index 3bbf52e..6b5fa18 100644
--- a/src/com/android/tradefed/device/metric/FilePullerDeviceMetricCollector.java
+++ b/src/com/android/tradefed/device/metric/FilePullerDeviceMetricCollector.java
@@ -28,6 +28,7 @@
 import java.io.IOException;
 import java.util.AbstractMap.SimpleEntry;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -121,9 +122,11 @@
             return;
         }
         for (String key : mKeys) {
-            Entry<String, File> pulledMetrics = pullMetricFile(key, currentMetrics);
-            if (pulledMetrics != null) {
-                processMetricFile(pulledMetrics.getKey(), pulledMetrics.getValue(), data);
+            Map<String, File> pulledMetrics = pullMetricFile(key, currentMetrics);
+
+            // Process all the metric files that matched the key pattern.
+            for (Map.Entry<String, File> entry : pulledMetrics.entrySet()) {
+                processMetricFile(entry.getKey(), entry.getValue(), data);
             }
         }
 
@@ -137,8 +140,9 @@
     }
 
 
-    private Entry<String, File> pullMetricFile(
+    private Map<String, File> pullMetricFile(
             String pattern, final Map<String, String> currentMetrics) {
+        Map<String, File> matchedFiles = new HashMap<>();
         Pattern p = Pattern.compile(pattern);
         for (Entry<String, String> entry : currentMetrics.entrySet()) {
             if (p.matcher(entry.getKey()).find()) {
@@ -153,8 +157,9 @@
                             if (mCleanUp) {
                                 device.deleteFile(entry.getValue());
                             }
-                            // Return the actual key and the file associated
-                            return new SimpleEntry<String, File>(entry.getKey(), attemptPull);
+                            // Store all the keys that matches the pattern and the corresponding
+                            // files pulled from the device.
+                            matchedFiles.put(entry.getKey(), attemptPull);
                         }
                     } catch (DeviceNotAvailableException e) {
                         CLog.e(
@@ -165,9 +170,13 @@
                 }
             }
         }
-        // Not a hard failure, just nice to know
-        CLog.d("Could not find a device file associated to pattern '%s'.", pattern);
-        return null;
+
+        if (matchedFiles.isEmpty()) {
+            // Not a hard failure, just nice to know
+            CLog.d("Could not find a device file associated to pattern '%s'.", pattern);
+
+        }
+        return matchedFiles;
     }
 
     /**
diff --git a/src/com/android/tradefed/invoker/InvocationExecution.java b/src/com/android/tradefed/invoker/InvocationExecution.java
index a1dcecf..395fe6e 100644
--- a/src/com/android/tradefed/invoker/InvocationExecution.java
+++ b/src/com/android/tradefed/invoker/InvocationExecution.java
@@ -18,7 +18,6 @@
 import com.android.ddmlib.IDevice;
 import com.android.ddmlib.Log.LogLevel;
 import com.android.tradefed.build.BuildInfo;
-import com.android.tradefed.build.BuildInfoKey;
 import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
 import com.android.tradefed.build.BuildRetrievalError;
 import com.android.tradefed.build.IBuildInfo;
@@ -42,6 +41,7 @@
 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.invoker.logger.TfObjectTracker;
 import com.android.tradefed.invoker.shard.IShardHelper;
+import com.android.tradefed.invoker.shard.TestsPoolPoller;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ByteArrayInputStreamSource;
@@ -157,7 +157,6 @@
                 // TODO: remove build update when reporting is done on context
                 updateBuild(info, config);
                 linkExternalDirs(info, testInfo);
-                info.setTestResourceBuild(config.isDeviceConfiguredFake(currentDeviceName));
 
                 if (config.getCommandOptions().shouldUseReplicateSetup()) {
                     buildReplicat = info;
@@ -172,7 +171,6 @@
             }
             throw e;
         }
-        createSharedResources(testInfo);
         setBinariesVersion(testInfo.getContext());
         return true;
     }
@@ -585,7 +583,9 @@
                 // Handle the no-retry use case
                 if (!decision.isAutoRetryEnabled()
                         || RetryStrategy.NO_RETRY.equals(decision.getRetryStrategy())
-                        || test instanceof ITestSuite) {
+                        || test instanceof ITestSuite
+                        // TODO: Handle auto-retry in local-sharding for non-suite
+                        || test instanceof TestsPoolPoller) {
                     runTest(config, info, listener, test);
                     remainingTests.remove(test);
                     continue;
@@ -869,39 +869,6 @@
         return null;
     }
 
-    /** Populate the shared resources directory for all non-resource build */
-    private void createSharedResources(TestInformation testInfo) {
-        List<IBuildInfo> infos = testInfo.getContext().getBuildInfos();
-        if (infos.size() <= 1) {
-            return;
-        }
-        try {
-            for (IBuildInfo info : infos) {
-                if (info.isTestResourceBuild()) {
-                    // Create a reception sub-folder for each build info resource to avoid mixing
-                    String name =
-                            String.format(
-                                    "%s_%s_%s",
-                                    info.getBuildBranch(),
-                                    info.getBuildId(),
-                                    info.getBuildFlavor());
-                    File buildDir = FileUtil.createTempDir(name, testInfo.dependenciesFolder());
-                    for (BuildInfoFileKey key : BuildInfoKey.SHARED_KEY) {
-                        File f = info.getFile(key);
-                        if (f == null) {
-                            continue;
-                        }
-                        File subDir = new File(buildDir, f.getName());
-                        FileUtil.symlinkFile(f, subDir);
-                    }
-                }
-            }
-        } catch (IOException e) {
-            CLog.e("Failed to create the shared resources dir.");
-            CLog.e(e);
-        }
-    }
-
     private void setBinariesVersion(IInvocationContext context) {
         String version = getAdbVersion();
         if (version != null) {
diff --git a/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java b/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
index d6a4be6..772075b 100644
--- a/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
+++ b/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
@@ -98,6 +98,7 @@
     private static final String MODULE_TAG = "Module";
     private static final String MODULES_DONE_ATTR = "modules_done";
     private static final String MODULES_TOTAL_ATTR = "modules_total";
+    private static final String MODULES_NOT_DONE_REASON = "Reason";
     private static final String NAME_ATTR = "name";
     private static final String OS_ARCH_ATTR = "os_arch";
     private static final String OS_NAME_ATTR = "os_name";
@@ -297,6 +298,15 @@
                     NS, PASS_ATTR, Integer.toString(module.getNumTestsInState(TestStatus.PASSED)));
             serializer.attribute(NS, TOTAL_TESTS_ATTR, Integer.toString(module.getNumTests()));
 
+            if (!isDone) {
+                String message = module.getRunFailureMessage();
+                if (message == null) {
+                    message = "Run was incomplete. Some tests might not have finished.";
+                }
+                serializer.startTag(NS, MODULES_NOT_DONE_REASON);
+                serializer.attribute(NS, MESSAGE_ATTR, message);
+                serializer.endTag(NS, MODULES_NOT_DONE_REASON);
+            }
             serializeTestCases(serializer, module.getTestResults());
             serializer.endTag(NS, MODULE_TAG);
         }
@@ -603,6 +613,13 @@
             module.testRunStarted(moduleId, totalTests);
             // TestCase level information parsing
             while (parser.nextTag() == XmlPullParser.START_TAG) {
+                // If a reason for not done exists, handle it.
+                if (parser.getName().equals(MODULES_NOT_DONE_REASON)) {
+                    parser.require(XmlPullParser.START_TAG, NS, MODULES_NOT_DONE_REASON);
+                    parser.nextTag();
+                    parser.require(XmlPullParser.END_TAG, NS, MODULES_NOT_DONE_REASON);
+                    continue;
+                }
                 parser.require(XmlPullParser.START_TAG, NS, CASE_TAG);
                 String className = parser.getAttributeValue(NS, NAME_ATTR);
                 // Test level information parsing
diff --git a/test_framework/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
index 9edbd10..31ffcc6 100644
--- a/test_framework/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
@@ -222,15 +222,14 @@
      * Extracts and returns splits for the specified apks.
      *
      * @param testInfo the {@link TestInformation}
-     * @param apksName The name of the apks file to extract splits from.
+     * @param moduleFile The module file to extract the splits from.
      * @return a File[] containing the splits.
      * @throws TargetSetupError if bundletool cannot be found or device spec file fails to generate.
      */
-    private File[] getSplitsForApks(TestInformation testInfo, String apksName)
+    private File[] getSplitsForApks(TestInformation testInfo, File moduleFile)
             throws TargetSetupError {
         initBundletoolUtil(testInfo);
         initDeviceSpecFilePath(testInfo.getDevice());
-        File moduleFile = getLocalPathForFilename(testInfo, apksName);
         File splitsDir =
                 getBundletoolUtil()
                         .extractSplitsFromApks(
@@ -274,7 +273,7 @@
             }
             String modulePackageName = "";
             if (moduleFile.getName().endsWith(SPLIT_APKS_SUFFIX)) {
-                File[] splits = getSplitsForApks(testInfo, moduleFileName);
+                File[] splits = getSplitsForApks(testInfo, moduleFile);
                 if (splits == null) {
                     // Bundletool failed to extract splits.
                     CLog.w(
@@ -437,7 +436,7 @@
             throws TargetSetupError, DeviceNotAvailableException {
         File apks = getLocalPathForFilename(testInfo, apksName);
         // Rename the extracted files and add the file to filename list.
-        File[] splits = getSplitsForApks(testInfo, apks.getName());
+        File[] splits = getSplitsForApks(testInfo, apks);
         ITestDevice device = testInfo.getDevice();
         if (splits.length == 0) {
             throw new TargetSetupError(
@@ -471,7 +470,7 @@
         for (String moduleFileName : testAppFileNames) {
             File moduleFile = getLocalPathForFilename(testInfo, moduleFileName);
             if (moduleFileName.endsWith(SPLIT_APKS_SUFFIX)) {
-                File[] splits = getSplitsForApks(testInfo, moduleFileName);
+                File[] splits = getSplitsForApks(testInfo, moduleFile);
                 String splitsArgs = createInstallArgsForSplit(splits, device);
                 mSplitsInstallArgs.add(splitsArgs);
             } else {
diff --git a/test_framework/com/android/tradefed/targetprep/RunHostScriptTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RunHostScriptTargetPreparer.java
new file mode 100644
index 0000000..0245523
--- /dev/null
+++ b/test_framework/com/android/tradefed/targetprep/RunHostScriptTargetPreparer.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.targetprep;
+
+import com.android.tradefed.config.GlobalConfiguration;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.IDeviceManager;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.RunUtil;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nullable;
+
+/**
+ * Target preparer which executes a script before running a test. The script can reference the
+ * device's serial number using the ANDROID_SERIAL environment variable.
+ */
+@OptionClass(alias = "run-host-script")
+public class RunHostScriptTargetPreparer extends BaseTargetPreparer {
+
+    @Option(name = "script-file", description = "Path to the script to execute.")
+    private String mScriptPath = null;
+
+    @Option(name = "work-dir", description = "Working directory to use when executing script.")
+    private File mWorkDir = null;
+
+    @Option(name = "script-timeout", description = "Script execution timeout.")
+    private Duration mTimeout = Duration.ofMinutes(1L);
+
+    private IRunUtil mRunUtil;
+
+    @Override
+    public void setUp(TestInformation testInfo)
+            throws TargetSetupError, BuildError, DeviceNotAvailableException {
+        if (mScriptPath == null) {
+            CLog.w("No script to execute.");
+            return;
+        }
+        ITestDevice device = testInfo.getDevice();
+
+        // Find script and verify it exists and is executable
+        File scriptFile = findScriptFile(testInfo);
+        if (scriptFile == null || !scriptFile.isFile()) {
+            throw new TargetSetupError(
+                    String.format("File %s not found.", mScriptPath), device.getDeviceDescriptor());
+        }
+        if (!scriptFile.canExecute()) {
+            scriptFile.setExecutable(true);
+        }
+
+        // Set working directory and environment variables
+        getRunUtil().setWorkingDir(mWorkDir);
+        getRunUtil().setEnvVariable("ANDROID_SERIAL", device.getSerialNumber());
+        setPathVariable(testInfo);
+
+        // Execute script and handle result
+        CommandResult result =
+                getRunUtil().runTimedCmd(mTimeout.toMillis(), scriptFile.getAbsolutePath());
+        switch (result.getStatus()) {
+            case SUCCESS:
+                CLog.i("Script executed successfully, stdout = [%s].", result.getStdout());
+                break;
+            case FAILED:
+                throw new TargetSetupError(
+                        String.format(
+                                "Script execution failed, stdout = [%s], stderr = [%s].",
+                                result.getStdout(), result.getStderr()),
+                        device.getDeviceDescriptor());
+            case TIMED_OUT:
+                throw new TargetSetupError(
+                        "Script execution timed out.", device.getDeviceDescriptor());
+            case EXCEPTION:
+                throw new TargetSetupError(
+                        String.format(
+                                "Exception during script execution, stdout = [%s], stderr = [%s].",
+                                result.getStdout(), result.getStderr()),
+                        device.getDeviceDescriptor());
+        }
+    }
+
+    /** @return {@link IRunUtil} instance to use */
+    @VisibleForTesting
+    IRunUtil getRunUtil() {
+        if (mRunUtil == null) {
+            mRunUtil = new RunUtil();
+        }
+        return mRunUtil;
+    }
+
+    /** @return {@link IDeviceManager} instance used to fetch the configured adb/fastboot paths */
+    @VisibleForTesting
+    IDeviceManager getDeviceManager() {
+        return GlobalConfiguration.getDeviceManagerInstance();
+    }
+
+    /** Find the script file to execute. */
+    @Nullable
+    private File findScriptFile(TestInformation testInfo) {
+        File scriptFile = new File(mScriptPath);
+        if (scriptFile.isAbsolute()) {
+            return scriptFile;
+        }
+        // Try to find the script in the working directory if it was set
+        if (mWorkDir != null) {
+            scriptFile = new File(mWorkDir, mScriptPath);
+            if (scriptFile.isFile()) {
+                return scriptFile;
+            }
+        }
+        // Otherwise, search for it in the test information
+        try {
+            return testInfo.getDependencyFile(mScriptPath, false);
+        } catch (FileNotFoundException e) {
+            return null;
+        }
+    }
+
+    /** Update $PATH if necessary to use consistent adb and fastboot binaries. */
+    private void setPathVariable(TestInformation testInfo) {
+        List<String> paths = new ArrayList<>();
+        // Use the test's adb binary or the globally defined one
+        File adbBinary = testInfo.executionFiles().get(FilesKey.ADB_BINARY);
+        if (adbBinary == null) {
+            String adbPath = getDeviceManager().getAdbPath();
+            if (!adbPath.equals("adb")) { // ignore default binary
+                adbBinary = new File(adbPath);
+            }
+        }
+        if (adbBinary != null && adbBinary.isFile()) {
+            paths.add(adbBinary.getParentFile().getAbsolutePath());
+        }
+        // Use the globally defined fastboot binary
+        String fastbootPath = getDeviceManager().getFastbootPath();
+        if (!fastbootPath.equals("fastboot")) { // ignore default binary
+            File fastbootBinary = new File(fastbootPath);
+            if (fastbootBinary.isFile()) {
+                paths.add(fastbootBinary.getParentFile().getAbsolutePath());
+            }
+        }
+        // Prepend additional paths to the PATH variable
+        if (!paths.isEmpty()) {
+            String separator = System.getProperty("path.separator");
+            paths.add(System.getenv("PATH"));
+            String path = paths.stream().distinct().collect(Collectors.joining(separator));
+            CLog.d("Using updated $PATH: %s", path);
+            getRunUtil().setEnvVariable("PATH", path);
+        }
+    }
+}
diff --git a/test_framework/com/android/tradefed/testtype/GTestListener.java b/test_framework/com/android/tradefed/testtype/GTestListener.java
index c967d06..38c6498 100644
--- a/test_framework/com/android/tradefed/testtype/GTestListener.java
+++ b/test_framework/com/android/tradefed/testtype/GTestListener.java
@@ -22,8 +22,10 @@
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -32,6 +34,7 @@
  */
 final class GTestListener extends ResultForwarder {
 
+    private static final int MAX_PARTIAL_SET_SIZE = 20;
     private Set<TestDescription> mTests = new HashSet<>();
     private Set<TestDescription> mDuplicateTests = new HashSet<>();
 
@@ -50,11 +53,19 @@
     @Override
     public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
         if (!mDuplicateTests.isEmpty()) {
-            FailureDescription error =
-                    FailureDescription.create(
-                            String.format(
-                                    "The following tests ran more than once: %s.",
-                                    mDuplicateTests));
+            StringBuilder errorMessage = new StringBuilder();
+            errorMessage.append(
+                    String.format("%s tests ran more than once.", mDuplicateTests.size()));
+            if (mDuplicateTests.size() > MAX_PARTIAL_SET_SIZE) {
+                List<TestDescription> partialDuplicateSet = new ArrayList<>(mDuplicateTests);
+                while (partialDuplicateSet.size() > MAX_PARTIAL_SET_SIZE) {
+                    partialDuplicateSet.remove(0);
+                }
+                errorMessage.append(String.format(" Partial list: %s", partialDuplicateSet));
+            } else {
+                errorMessage.append(String.format(" Full list: %s", mDuplicateTests));
+            }
+            FailureDescription error = FailureDescription.create(errorMessage.toString());
             error.setFailureStatus(FailureStatus.TEST_FAILURE);
             super.testRunFailed(error);
         }
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 3dabdfc..101caa8 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -23,7 +23,6 @@
 import com.android.tradefed.build.DeviceBuildInfoTest;
 import com.android.tradefed.build.DeviceFolderBuildInfoTest;
 import com.android.tradefed.build.FileDownloadCacheTest;
-import com.android.tradefed.build.GCSTestResourceProviderTest;
 import com.android.tradefed.build.LocalDeviceBuildProviderTest;
 import com.android.tradefed.build.OtaZipfileBuildProviderTest;
 import com.android.tradefed.clearcut.ClearcutClientTest;
@@ -236,6 +235,7 @@
 import com.android.tradefed.targetprep.RootTargetPreparerTest;
 import com.android.tradefed.targetprep.RunCommandTargetPreparerTest;
 import com.android.tradefed.targetprep.RunHostCommandTargetPreparerTest;
+import com.android.tradefed.targetprep.RunHostScriptTargetPreparerTest;
 import com.android.tradefed.targetprep.StopServicesSetupTest;
 import com.android.tradefed.targetprep.SwitchUserTargetPreparerTest;
 import com.android.tradefed.targetprep.SystemUpdaterDeviceFlasherTest;
@@ -415,7 +415,6 @@
     DeviceBuildDescriptorTest.class,
     DeviceFolderBuildInfoTest.class,
     FileDownloadCacheTest.class,
-    GCSTestResourceProviderTest.class,
     LocalDeviceBuildProviderTest.class,
     OtaZipfileBuildProviderTest.class,
 
@@ -676,6 +675,7 @@
     RootTargetPreparerTest.class,
     RunCommandTargetPreparerTest.class,
     RunHostCommandTargetPreparerTest.class,
+    RunHostScriptTargetPreparerTest.class,
     StopServicesSetupTest.class,
     SystemUpdaterDeviceFlasherTest.class,
     TargetSetupErrorTest.class,
diff --git a/tests/src/com/android/tradefed/build/BuildInfoTest.java b/tests/src/com/android/tradefed/build/BuildInfoTest.java
index 1f7b1a0..9eed940 100644
--- a/tests/src/com/android/tradefed/build/BuildInfoTest.java
+++ b/tests/src/com/android/tradefed/build/BuildInfoTest.java
@@ -28,14 +28,12 @@
 import com.android.tradefed.util.SerializationUtil;
 
 import org.junit.After;
-import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 import java.io.File;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
@@ -211,28 +209,4 @@
             assertTrue(deserialized.getRemoteFiles().contains(new File(file)));
         }
     }
-
-    /** Test {@link BuildInfo#getTestResource(List, String)} */
-    @Test
-    public void testGetTestResource() {
-        List<IBuildInfo> buildInfos = new ArrayList<IBuildInfo>();
-        BuildInfo testResourceBuild = new BuildInfo();
-        testResourceBuild.setTestResourceBuild(true);
-        File testResourceFile = new File("test-resource1");
-        testResourceBuild.setFile("test-resource1", testResourceFile, "");
-        buildInfos.add(testResourceBuild);
-        File file = BuildInfo.getTestResource(buildInfos, "test-resource1");
-        Assert.assertEquals(testResourceFile, file);
-    }
-
-    /** Test {@link BuildInfo#getTestResource(List, String)} */
-    @Test
-    public void testGetTestResource_notExist() {
-        List<IBuildInfo> buildInfos = new ArrayList<IBuildInfo>();
-        BuildInfo testResourceBuild = new BuildInfo();
-        testResourceBuild.setTestResourceBuild(true);
-        buildInfos.add(testResourceBuild);
-        File file = BuildInfo.getTestResource(buildInfos, "test-resource1");
-        Assert.assertNull(file);
-    }
 }
diff --git a/tests/src/com/android/tradefed/build/GCSTestResourceProviderTest.java b/tests/src/com/android/tradefed/build/GCSTestResourceProviderTest.java
deleted file mode 100644
index ad93333..0000000
--- a/tests/src/com/android/tradefed/build/GCSTestResourceProviderTest.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.build;
-
-import com.android.tradefed.build.gcs.GCSDownloaderHelper;
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.util.FileUtil;
-
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.io.File;
-import java.io.IOException;
-
-/** Unit test for {@link GCSTestResourceProvider}. */
-@RunWith(JUnit4.class)
-public class GCSTestResourceProviderTest {
-
-    private static final String TEST_RESOURCE1 = "gs://b/this/is/a/file1.txt";
-    private static final String TEST_RESOURCE2 = "gs://b/this/is/a/file2.txt";
-
-    private File mRoot;
-    private GCSTestResourceProvider mTestResourceProvider;
-
-    @Before
-    public void setUp() throws Exception {
-        mRoot = FileUtil.createTempDir(GCSTestResourceProviderTest.class.getSimpleName());
-        mTestResourceProvider =
-                new GCSTestResourceProvider() {
-                    @Override
-                    GCSDownloaderHelper getHelper() {
-                        return new GCSDownloaderHelper() {
-                            @Override
-                            public File fetchTestResource(String gsPath)
-                                    throws BuildRetrievalError {
-                                try {
-                                    File f = FileUtil.createTempFile("test-gcs-file", "txt");
-                                    FileUtil.writeToFile(gsPath, f);
-                                    return f;
-                                } catch (IOException e) {
-                                    throw new BuildRetrievalError(e.getMessage(), e);
-                                }
-                            }
-                        };
-                    }
-                };
-        OptionSetter setter = new OptionSetter(mTestResourceProvider);
-        setter.setOptionValue("test-resource", "key1", TEST_RESOURCE1);
-        setter.setOptionValue("test-resource", "key2", TEST_RESOURCE2);
-    }
-
-    @After
-    public void tearDown() {
-        FileUtil.recursiveDelete(mRoot);
-    }
-
-    @Test
-    public void testGetBuild() throws Exception {
-        IBuildInfo buildInfo = mTestResourceProvider.getBuild();
-        File file1 = buildInfo.getFile("key1");
-        File file2 = buildInfo.getFile("key2");
-        Assert.assertEquals(TEST_RESOURCE1, FileUtil.readStringFromFile(file1));
-        Assert.assertEquals(TEST_RESOURCE2, FileUtil.readStringFromFile(file2));
-        mTestResourceProvider.cleanUp(buildInfo);
-        Assert.assertFalse(file1.exists());
-        Assert.assertFalse(file2.exists());
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/cloud/GceManagerTest.java b/tests/src/com/android/tradefed/device/cloud/GceManagerTest.java
index f3bdced..cae19ce 100644
--- a/tests/src/com/android/tradefed/device/cloud/GceManagerTest.java
+++ b/tests/src/com/android/tradefed/device/cloud/GceManagerTest.java
@@ -40,7 +40,6 @@
 import org.easymock.Capture;
 import org.easymock.EasyMock;
 import org.junit.After;
-import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -79,7 +78,7 @@
         mOptions.setAvdDriverBinary(mAvdBinary);
         mOptions.setAvdConfigFile(mAvdBinary);
         mGceManager =
-                new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo, null) {
+                new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo) {
                     @Override
                     IRunUtil getRunUtil() {
                         return mMockRunUtil;
@@ -249,7 +248,7 @@
             setter.setOptionValue("gce-driver-param", "--emulator-build-id");
             setter.setOptionValue("gce-driver-param", "EMULATOR_BUILD_ID");
             mGceManager =
-                    new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo, null) {
+                    new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo) {
                         @Override
                         IRunUtil getRunUtil() {
                             return mMockRunUtil;
@@ -331,7 +330,7 @@
         // Boot-time on Acloud params will be overridden by TF option.
         setter.setOptionValue("allow-gce-boot-timeout-override", "false");
         mGceManager =
-                new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo, null) {
+                new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo) {
                     @Override
                     IRunUtil getRunUtil() {
                         return mMockRunUtil;
@@ -418,7 +417,7 @@
         OptionSetter setter = new OptionSetter(mOptions);
         setter.setOptionValue("allow-gce-boot-timeout-override", "true");
         mGceManager =
-                new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo, null) {
+                new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo) {
                     @Override
                     IRunUtil getRunUtil() {
                         return mMockRunUtil;
@@ -475,7 +474,7 @@
         OptionSetter setter = new OptionSetter(mOptions);
         setter.setOptionValue("allow-gce-boot-timeout-override", "true");
         mGceManager =
-                new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo, null) {
+                new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo) {
                     @Override
                     IRunUtil getRunUtil() {
                         return mMockRunUtil;
@@ -517,7 +516,7 @@
         OptionSetter setter = new OptionSetter(mOptions);
         setter.setOptionValue("allow-gce-boot-timeout-override", "true");
         mGceManager =
-                new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo, null) {
+                new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo) {
                     @Override
                     IRunUtil getRunUtil() {
                         return mMockRunUtil;
@@ -572,8 +571,7 @@
     @Test
     public void testShutdownGce() throws Exception {
         mGceManager =
-                new GceManager(
-                        mMockDeviceDesc, mOptions, mMockBuildInfo, null, "instance1", "host1") {
+                new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo, "instance1", "host1") {
                     @Override
                     IRunUtil getRunUtil() {
                         return mMockRunUtil;
@@ -611,8 +609,7 @@
         OptionSetter setter = new OptionSetter(mOptions);
         setter.setOptionValue("wait-gce-teardown", "false");
         mGceManager =
-                new GceManager(
-                        mMockDeviceDesc, mOptions, mMockBuildInfo, null, "instance1", "host1") {
+                new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo, "instance1", "host1") {
                     @Override
                     IRunUtil getRunUtil() {
                         return mMockRunUtil;
@@ -645,8 +642,7 @@
     @Test
     public void testShutdownGce_withJsonKeyFile() throws Exception {
         mGceManager =
-                new GceManager(
-                        mMockDeviceDesc, mOptions, mMockBuildInfo, null, "instance1", "host1") {
+                new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo, "instance1", "host1") {
                     @Override
                     IRunUtil getRunUtil() {
                         return mMockRunUtil;
@@ -854,7 +850,7 @@
         setter.setOptionValue("allow-gce-boot-timeout-override", "true");
         DeviceDescriptor desc = null;
         mGceManager =
-                new GceManager(desc, mOptions, mMockBuildInfo, null) {
+                new GceManager(desc, mOptions, mMockBuildInfo) {
                     @Override
                     IRunUtil getRunUtil() {
                         return mMockRunUtil;
@@ -916,28 +912,6 @@
         EasyMock.verify(mMockRunUtil);
     }
 
-    /**
-     * Test {@link GceManager#getAvdConfigFile()} while using test resource.
-     *
-     * @throws Exception
-     */
-    @Test
-    public void testGetAvdConfigFile_testResource() throws Exception {
-        OptionSetter setter = new OptionSetter(mOptions);
-        setter.setOptionValue("gce-driver-config-test-resource-name", "device.config");
-
-        BuildInfo testResourceBuild = new BuildInfo();
-        testResourceBuild.setTestResourceBuild(true);
-        File configFile = new File("device.config");
-        testResourceBuild.setFile("device.config", configFile, "");
-        List<IBuildInfo> testResourceBuildInfos = new ArrayList<>();
-        testResourceBuildInfos.add(testResourceBuild);
-
-        mGceManager =
-                new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo, testResourceBuildInfos);
-        Assert.assertEquals(configFile, mGceManager.getAvdConfigFile());
-    }
-
     @Test
     public void testUpdateTimeout() throws Exception {
         OptionSetter setter = new OptionSetter(mOptions);
@@ -945,7 +919,7 @@
         mOptions.getGceDriverParams().add("--boot-timeout");
         mOptions.getGceDriverParams().add("900");
         assertEquals(1800000L, mOptions.getGceCmdTimeout());
-        mGceManager = new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo, null);
+        mGceManager = new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo);
         assertEquals(1080000L, mOptions.getGceCmdTimeout());
     }
 
@@ -958,7 +932,7 @@
         mOptions.getGceDriverParams().add("--boot-timeout");
         mOptions.getGceDriverParams().add("450");
         assertEquals(1800000L, mOptions.getGceCmdTimeout());
-        mGceManager = new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo, null);
+        mGceManager = new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo);
         // The last specified boot-timeout is used.
         assertEquals(630000L, mOptions.getGceCmdTimeout());
     }
@@ -970,7 +944,7 @@
         mOptions.getGceDriverParams().add("--someargs");
         mOptions.getGceDriverParams().add("900");
         assertEquals(1800000L, mOptions.getGceCmdTimeout());
-        mGceManager = new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo, null);
+        mGceManager = new GceManager(mMockDeviceDesc, mOptions, mMockBuildInfo);
         assertEquals(1800000L, mOptions.getGceCmdTimeout());
     }
 }
diff --git a/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java b/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
index 6fa4e2b..e8d0219 100644
--- a/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
@@ -174,10 +174,7 @@
                     @Override
                     GceManager getGceHandler() {
                         return new GceManager(
-                                getDeviceDescriptor(),
-                                new TestDeviceOptions(),
-                                mMockBuildInfo,
-                                null) {
+                                getDeviceDescriptor(), new TestDeviceOptions(), mMockBuildInfo) {
                             @Override
                             protected List<String> buildGceCmd(
                                     File reportFile, IBuildInfo b, String ipDevice) {
diff --git a/tests/src/com/android/tradefed/device/metric/FilePullerDeviceMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/FilePullerDeviceMetricCollectorTest.java
index 27e9203..ec04f22 100644
--- a/tests/src/com/android/tradefed/device/metric/FilePullerDeviceMetricCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/FilePullerDeviceMetricCollectorTest.java
@@ -95,6 +95,35 @@
     }
 
     /**
+     * Test when a multiple file is found matching the matching key, then pulled and {@link
+     * FilePullerDeviceMetricCollector#processMetricFile(String, File, DeviceMetricData)} is called
+     * multiple times.
+     */
+    @Test
+    public void testPullMultipleMatchingKeyInMetrics() throws Exception {
+        OptionSetter setter = new OptionSetter(mFilePuller);
+        setter.setOptionValue("pull-pattern-keys", "coverageFile");
+        HashMap<String, Metric> currentMetrics = new HashMap<>();
+        currentMetrics.put("coverageFile", TfMetricProtoUtil.stringToMetric("/data/coverage1"));
+        currentMetrics.put("coverageFileAnother",
+                TfMetricProtoUtil.stringToMetric("/data/coverage2"));
+
+        Mockito.when(mMockDevice.pullFile(Mockito.eq("/data/coverage1")))
+                .thenReturn(new File("fake1"));
+        Mockito.when(mMockDevice.pullFile(Mockito.eq("/data/coverage2")))
+                .thenReturn(new File("fake2"));
+
+        mFilePuller.testRunStarted("fakeRun", 5);
+        mFilePuller.testRunEnded(500, currentMetrics);
+
+        Mockito.verify(mMockListener)
+                .testLog(Mockito.eq("coverageFile"), Mockito.eq(LogDataType.TEXT), Mockito.any());
+        Mockito.verify(mMockListener)
+                .testLog(Mockito.eq("coverageFileAnother"), Mockito.eq(LogDataType.TEXT),
+                        Mockito.any());
+    }
+
+    /**
      * Test when a file is found matching the key using a pattern matching, then pulled and {@link
      * FilePullerDeviceMetricCollector#processMetricFile(String, File, DeviceMetricData)} is called.
      */
diff --git a/tests/src/com/android/tradefed/invoker/InvocationExecutionTest.java b/tests/src/com/android/tradefed/invoker/InvocationExecutionTest.java
index 8b84e35..4d743ac 100644
--- a/tests/src/com/android/tradefed/invoker/InvocationExecutionTest.java
+++ b/tests/src/com/android/tradefed/invoker/InvocationExecutionTest.java
@@ -24,10 +24,6 @@
 import static org.mockito.Mockito.verify;
 
 import com.android.tradefed.build.BuildInfo;
-import com.android.tradefed.build.BuildRetrievalError;
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.build.IBuildProvider;
-import com.android.tradefed.build.StubBuildProvider;
 import com.android.tradefed.config.Configuration;
 import com.android.tradefed.config.DeviceConfigurationHolder;
 import com.android.tradefed.config.IConfiguration;
@@ -55,7 +51,6 @@
 import com.android.tradefed.targetprep.multi.IMultiTargetPreparer;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.suite.TestSuiteStub;
-import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.IDisableable;
 
 import org.easymock.EasyMock;
@@ -66,7 +61,6 @@
 import org.mockito.InOrder;
 import org.mockito.Mockito;
 
-import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
@@ -86,7 +80,6 @@
     private ILogSaver mLogSaver;
     private ITestInvocationListener mMockListener;
     private ILogSaverListener mMockLogListener;
-    private ITestDevice mMockDevice;
     private ITestLogger mMockLogger;
 
     @Before
@@ -99,7 +92,6 @@
         mMockListener = mock(ITestInvocationListener.class);
         mMockLogListener = mock(ILogSaverListener.class);
         mMockLogger = mock(ITestLogger.class);
-        mMockDevice = EasyMock.createMock(ITestDevice.class);
         // Reset the counters
         TestBaseMetricCollector.sTotalInit = 0;
         RemoteTestCollector.sTotalInit = 0;
@@ -562,68 +554,4 @@
         inOrder.verify(stub1).isDisabled();
         inOrder.verify(stub1).tearDown(testInfo, exception);
     }
-
-    /** Ensure we create the shared folder from the resource build. */
-    @Test
-    public void testFetchBuild_createSharedFolder() throws Throwable {
-        mExec =
-                new InvocationExecution() {
-                    @Override
-                    protected String getAdbVersion() {
-                        return "1";
-                    }
-                };
-        EasyMock.expect(mMockDevice.getSerialNumber()).andStubReturn("serial");
-        mMockDevice.setRecovery(EasyMock.anyObject());
-        EasyMock.expectLastCall().times(2);
-
-        List<IDeviceConfiguration> listDeviceConfig = new ArrayList<>();
-        DeviceConfigurationHolder holder = new DeviceConfigurationHolder("device1");
-        IBuildProvider provider = new StubBuildProvider();
-        holder.addSpecificConfig(provider);
-        mContext.addAllocatedDevice("device1", mMockDevice);
-        listDeviceConfig.add(holder);
-
-        DeviceConfigurationHolder holder2 = new DeviceConfigurationHolder("device2", true);
-        IBuildProvider provider2 =
-                new StubBuildProvider() {
-                    @Override
-                    public IBuildInfo getBuild() throws BuildRetrievalError {
-                        IBuildInfo info = super.getBuild();
-                        info.setBuildId("1234");
-                        info.setBuildBranch("branch");
-                        info.setBuildFlavor("flavor");
-                        return info;
-                    }
-                };
-        holder2.addSpecificConfig(provider2);
-        mContext.addAllocatedDevice("device2", mMockDevice);
-        listDeviceConfig.add(holder2);
-
-        mConfig.setDeviceConfigList(listDeviceConfig);
-        File tmpWorkDir = FileUtil.createTempDir("invocation-execution-shared-test");
-        TestInformation testInfo =
-                TestInformation.newBuilder()
-                        .setInvocationContext(mContext)
-                        .setDependenciesFolder(tmpWorkDir)
-                        .build();
-        // Download
-        EasyMock.replay(mMockDevice);
-        assertTrue(mExec.fetchBuild(testInfo, mConfig, null, mMockListener));
-        EasyMock.verify(mMockDevice);
-
-        List<IBuildInfo> builds = mContext.getBuildInfos();
-        try {
-            assertEquals(2, builds.size());
-            assertEquals(1, tmpWorkDir.listFiles().length);
-            // The resource build was linked to dependencies
-            assertTrue(tmpWorkDir.listFiles()[0].getName().startsWith("branch_1234_flavor"));
-        } finally {
-            for (IBuildInfo info : builds) {
-                info.cleanUp();
-            }
-            FileUtil.recursiveDelete(tmpWorkDir);
-        }
-        assertTrue(mContext.getAttributes().containsKey(InvocationExecution.JAVA_VERSION_KEY));
-    }
 }
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
index 9257c88..1eb26ee 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
@@ -130,7 +130,6 @@
         mProvider1 = EasyMock.createMock(IBuildProvider.class);
         holder1.addSpecificConfig(mProvider1);
         EasyMock.expect(mMockConfig.getDeviceConfigByName("device1")).andStubReturn(holder1);
-        EasyMock.expect(mMockConfig.isDeviceConfiguredFake("device1")).andReturn(false);
         mDevice1.setOptions(EasyMock.anyObject());
         mDevice1.setRecovery(EasyMock.anyObject());
 
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
index 8f4044c..1925909 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
@@ -242,8 +242,6 @@
         EasyMock.expect(mMockBuildInfo.getBuildFlavor()).andStubReturn("flavor");
         EasyMock.expect(mMockBuildInfo.getProperties()).andStubReturn(new HashSet<>());
         EasyMock.expect(mMockBuildInfo.isTestResourceBuild()).andStubReturn(false);
-        mMockBuildInfo.setTestResourceBuild(EasyMock.anyBoolean());
-        EasyMock.expectLastCall().anyTimes();
 
         // always expect logger initialization and cleanup calls
         mMockLogRegistry.registerLogger(mMockLogger);
diff --git a/tests/src/com/android/tradefed/result/suite/XmlSuiteResultFormatterTest.java b/tests/src/com/android/tradefed/result/suite/XmlSuiteResultFormatterTest.java
index 7eb74c6..647db0b 100644
--- a/tests/src/com/android/tradefed/result/suite/XmlSuiteResultFormatterTest.java
+++ b/tests/src/com/android/tradefed/result/suite/XmlSuiteResultFormatterTest.java
@@ -16,6 +16,7 @@
 package com.android.tradefed.result.suite;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
@@ -184,6 +185,62 @@
         assertEquals(holder.runResults.size(), mResultHolder.runResults.size());
     }
 
+    @Test
+    public void testFailuresReporting_notDone() throws Exception {
+        mResultHolder.context = mContext;
+
+        List<TestRunResult> runResults = new ArrayList<>();
+        runResults.add(createFakeResult("module1", 2, 1, 0, 0));
+        runResults.get(0).testRunFailed("Failed module");
+        mResultHolder.runResults = runResults;
+
+        Map<String, IAbi> modulesAbi = new HashMap<>();
+        modulesAbi.put("module1", new Abi("armeabi-v7a", "32"));
+        mResultHolder.modulesAbi = modulesAbi;
+
+        mResultHolder.completeModules = 2;
+        mResultHolder.totalModules = 1;
+        mResultHolder.passedTests = 2;
+        mResultHolder.failedTests = 1;
+        mResultHolder.startTime = 0L;
+        mResultHolder.endTime = 10L;
+        File res = mFormatter.writeResults(mResultHolder, mResultDir);
+        String content = FileUtil.readStringFromFile(res);
+
+        assertXmlContainsNode(content, "Result/Module");
+        assertXmlContainsAttribute(content, "Result/Module", "done", "false");
+        assertXmlContainsNode(content, "Result/Module/Reason");
+        assertXmlContainsAttribute(content, "Result/Module/TestCase", "name", "com.class.module1");
+        assertXmlContainsAttribute(
+                content, "Result/Module/TestCase/Test", "name", "module1.method0");
+        assertXmlContainsAttribute(
+                content, "Result/Module/TestCase/Test", "name", "module1.method1");
+        // Check that failures are showing in the xml for the test cases
+        assertXmlContainsAttribute(
+                content, "Result/Module/TestCase/Test", "name", "module1.failed0");
+        assertXmlContainsAttribute(content, "Result/Module/TestCase/Test", "result", "fail");
+        assertXmlContainsAttribute(
+                content, "Result/Module/TestCase/Test/Failure", "message", "module1 failed.");
+        assertXmlContainsValue(
+                content,
+                "Result/Module/TestCase/Test/Failure/StackTrace",
+                XmlSuiteResultFormatter.sanitizeXmlContent("module1 failed.\nstack\nstack\0"));
+        // Test that we can read back the informations
+        SuiteResultHolder holder = mFormatter.parseResults(mResultDir, false);
+        assertEquals(holder.completeModules, mResultHolder.completeModules);
+        assertEquals(holder.totalModules, mResultHolder.totalModules);
+        assertEquals(holder.passedTests, mResultHolder.passedTests);
+        assertEquals(holder.failedTests, mResultHolder.failedTests);
+        assertEquals(holder.startTime, mResultHolder.startTime);
+        assertEquals(holder.endTime, mResultHolder.endTime);
+        assertEquals(
+                holder.modulesAbi.get("armeabi-v7a module1"),
+                mResultHolder.modulesAbi.get("module1"));
+        assertEquals(1, holder.runResults.size());
+        TestRunResult result = new ArrayList<>(holder.runResults).get(0);
+        assertFalse(result.isRunComplete());
+    }
+
     /** Test that assumption failures and ignored tests are correctly reported in the xml. */
     @Test
     public void testAssumptionFailures_Ignore_Reporting() throws Exception {
diff --git a/tests/src/com/android/tradefed/targetprep/RunHostScriptTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/RunHostScriptTargetPreparerTest.java
new file mode 100644
index 0000000..4125fa1
--- /dev/null
+++ b/tests/src/com/android/tradefed/targetprep/RunHostScriptTargetPreparerTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.targetprep;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.IDeviceManager;
+import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.IRunUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/** Unit test for {@link RunHostScriptTargetPreparer}. */
+@RunWith(JUnit4.class)
+public final class RunHostScriptTargetPreparerTest {
+
+    private static final String DEVICE_SERIAL = "DEVICE_SERIAL";
+
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private TestInformation mTestInfo;
+
+    @Mock private IRunUtil mRunUtil;
+    @Mock private IDeviceManager mDeviceManager;
+    private RunHostScriptTargetPreparer mPreparer;
+    private OptionSetter mOptionSetter;
+    private File mWorkDir;
+    private File mScriptFile;
+
+    @Before
+    public void setUp() throws IOException, ConfigurationException {
+        // Initialize preparer and test information
+        mPreparer =
+                new RunHostScriptTargetPreparer() {
+                    @Override
+                    IRunUtil getRunUtil() {
+                        return mRunUtil;
+                    }
+
+                    @Override
+                    IDeviceManager getDeviceManager() {
+                        return mDeviceManager;
+                    }
+                };
+        mOptionSetter = new OptionSetter(mPreparer);
+        when(mTestInfo.getDevice().getSerialNumber()).thenReturn(DEVICE_SERIAL);
+        when(mTestInfo.executionFiles().get(any(FilesKey.class))).thenReturn(null);
+
+        // Create temporary working directory and script file
+        mWorkDir = FileUtil.createTempDir(this.getClass().getSimpleName());
+        mScriptFile = File.createTempFile("script", ".sh", mWorkDir);
+
+        // Default to default adb/fastboot paths
+        when(mDeviceManager.getAdbPath()).thenReturn("adb");
+        when(mDeviceManager.getFastbootPath()).thenReturn("fastboot");
+
+        // Default to successful execution
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        when(mRunUtil.runTimedCmd(anyLong(), any())).thenReturn(result);
+    }
+
+    @After
+    public void tearDown() {
+        FileUtil.recursiveDelete(mWorkDir);
+    }
+
+    @Test
+    public void testSetUp() throws Exception {
+        mOptionSetter.setOptionValue("script-file", mScriptFile.getAbsolutePath());
+        mOptionSetter.setOptionValue("script-timeout", "10");
+        // Verify environment, timeout, and script path
+        mPreparer.setUp(mTestInfo);
+        verify(mRunUtil).setEnvVariable("ANDROID_SERIAL", DEVICE_SERIAL);
+        verify(mRunUtil, never()).setEnvVariable(eq("PATH"), any()); // uses default PATH
+        verify(mRunUtil).runTimedCmd(10L, mScriptFile.getAbsolutePath());
+        // Verify that script is executable
+        assertTrue(mScriptFile.canExecute());
+    }
+
+    @Test
+    public void testSetUp_workingDir() throws Exception {
+        mOptionSetter.setOptionValue("work-dir", mWorkDir.getAbsolutePath());
+        mOptionSetter.setOptionValue("script-file", mScriptFile.getName()); // relative
+        // Verify that the working directory is set and script's path is resolved
+        mPreparer.setUp(mTestInfo);
+        verify(mRunUtil).setWorkingDir(mWorkDir);
+        verify(mRunUtil).runTimedCmd(anyLong(), eq(mScriptFile.getAbsolutePath()));
+    }
+
+    @Test
+    public void testSetUp_findFile() throws Exception {
+        mOptionSetter.setOptionValue("script-file", mScriptFile.getName()); // relative
+        when(mTestInfo.getDependencyFile(any(), anyBoolean())).thenReturn(mScriptFile);
+        // Verify that the script is found in the test information
+        mPreparer.setUp(mTestInfo);
+        verify(mRunUtil).runTimedCmd(anyLong(), eq(mScriptFile.getAbsolutePath()));
+    }
+
+    @Test(expected = TargetSetupError.class)
+    public void testSetUp_fileNotFound() throws Exception {
+        mOptionSetter.setOptionValue("script-file", "unknown.sh");
+        mPreparer.setUp(mTestInfo);
+    }
+
+    @Test(expected = TargetSetupError.class)
+    public void testSetUp_executionError() throws Exception {
+        mOptionSetter.setOptionValue("script-file", mScriptFile.getAbsolutePath());
+        CommandResult result = new CommandResult(CommandStatus.FAILED);
+        when(mRunUtil.runTimedCmd(anyLong(), any())).thenReturn(result);
+        mPreparer.setUp(mTestInfo);
+    }
+
+    @Test
+    public void testSetUp_pathVariable() throws Exception {
+        mOptionSetter.setOptionValue("script-file", mScriptFile.getAbsolutePath());
+        // Create and set dummy adb binary
+        Path adbDir = Files.createTempDirectory(mWorkDir.toPath(), "adb");
+        File adbBinary = File.createTempFile("adb", ".sh", adbDir.toFile());
+        when(mTestInfo.executionFiles().get(eq(FilesKey.ADB_BINARY))).thenReturn(adbBinary);
+        // Create and set dummy fastboot binary
+        Path fastbootDir = Files.createTempDirectory(mWorkDir.toPath(), "fastboot");
+        File fastbootBinary = File.createTempFile("fastboot", ".sh", fastbootDir.toFile());
+        when(mDeviceManager.getFastbootPath()).thenReturn(fastbootBinary.getAbsolutePath());
+        // Verify that binary paths were prepended to the path variable
+        String separator = System.getProperty("path.separator");
+        String expectedPath = adbDir + separator + fastbootDir + separator + System.getenv("PATH");
+        mPreparer.setUp(mTestInfo);
+        verify(mRunUtil).setEnvVariable("PATH", expectedPath);
+    }
+}
diff --git a/tests/src/com/android/tradefed/testtype/GTestListenerTest.java b/tests/src/com/android/tradefed/testtype/GTestListenerTest.java
index 392ee75..d05f3fb 100644
--- a/tests/src/com/android/tradefed/testtype/GTestListenerTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestListenerTest.java
@@ -96,7 +96,7 @@
         String moduleName = "testWithDuplicateTests";
         String testClass = "testClass";
         String testName1 = "testName1";
-        String duplicateTestsMessage = "The following tests ran more than once: ";
+        String duplicateTestsMessage = "1 tests ran more than once. Full list:";
         TestDescription testId1 = new TestDescription(testClass, testName1);
 
         mMockListener.testRunStarted(EasyMock.eq(moduleName), EasyMock.eq(2));