Merge "Add option to not rescale device screenshot."
diff --git a/prod-tests/src/com/android/ota/tests/SideloadOtaStabilityTest.java b/prod-tests/src/com/android/ota/tests/SideloadOtaStabilityTest.java
index 018bc4b..e77b70b 100644
--- a/prod-tests/src/com/android/ota/tests/SideloadOtaStabilityTest.java
+++ b/prod-tests/src/com/android/ota/tests/SideloadOtaStabilityTest.java
@@ -20,8 +20,6 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.junit.Assert.assertThat;
 
-import com.android.ddmlib.AdbCommandRejectedException;
-import com.android.ddmlib.TimeoutException;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.build.IDeviceBuildInfo;
 import com.android.tradefed.build.OtaDeviceBuildInfo;
@@ -46,6 +44,7 @@
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.testtype.IResumableTest;
 import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.DeviceRecoveryModeUtil;
 import com.android.tradefed.util.StreamUtil;
 
 import org.junit.Assert;
@@ -227,20 +226,8 @@
                 // so we should just reboot in that case since we no longer need to be in
                 // recovery
                 CLog.i("Device is not online, attempting to recover before capturing logs");
-                if (managedDevice.getDeviceState().equals(TestDeviceState.RECOVERY)) {
-                    CLog.i("Rebooting to exit recovery");
-                    try {
-                        // we don't want to enable root until the reboot is fully finished and
-                        // the device is available, or it may get stuck in recovery and time out
-                        managedDevice.getIDevice().reboot(null);
-                        managedDevice.waitForDeviceAvailable(mMaxRebootTimeSec * 1000);
-                        managedDevice.postBootSetup();
-                    } catch (TimeoutException | AdbCommandRejectedException | IOException e) {
-                        CLog.e("Failed to reboot, trying last-ditch recovery");
-                        CLog.e(e);
-                        managedDevice.recoverDevice();
-                    }
-                }
+                DeviceRecoveryModeUtil.bootOutOfRecovery((IManagedTestDevice) mDevice,
+                        mMaxInstallOnlineTimeSec * 1000);
             }
             double updateTime = sendRecoveryLog(listener);
             Map<String, String> metrics = new HashMap<String, String>(1);
diff --git a/src/com/android/tradefed/command/CommandFileWatcher.java b/src/com/android/tradefed/command/CommandFileWatcher.java
index a01589e..3d53d9d 100644
--- a/src/com/android/tradefed/command/CommandFileWatcher.java
+++ b/src/com/android/tradefed/command/CommandFileWatcher.java
@@ -145,6 +145,7 @@
      */
     public void cancel() {
         mCancelled = true;
+        interrupt();
     }
 
     /**
diff --git a/src/com/android/tradefed/command/CommandScheduler.java b/src/com/android/tradefed/command/CommandScheduler.java
index d108490..81ceb93 100644
--- a/src/com/android/tradefed/command/CommandScheduler.java
+++ b/src/com/android/tradefed/command/CommandScheduler.java
@@ -67,6 +67,8 @@
 import com.android.tradefed.util.TableFormatter;
 import com.android.tradefed.util.TimeUtil;
 import com.android.tradefed.util.hostmetric.IHostMonitor;
+import com.android.tradefed.util.hostmetric.IHostMonitor.HostDataPoint;
+import com.android.tradefed.util.hostmetric.IHostMonitor.HostMetricType;
 import com.android.tradefed.util.keystore.IKeyStoreClient;
 import com.android.tradefed.util.keystore.IKeyStoreFactory;
 import com.android.tradefed.util.keystore.KeyStoreException;
@@ -504,6 +506,7 @@
          * for the next iteration of testing.
          */
         private static final long CHECK_WAIT_DEVICE_AVAIL_MS = 30 * 1000;
+        private static final int EXPECTED_THREAD_COUNT = 1;
 
         private final IScheduledInvocationListener[] mListeners;
         private final IInvocationContext mInvocationContext;
@@ -599,6 +602,8 @@
                     device.setRecoveryMode(RecoveryMode.AVAILABLE);
                 }
 
+                checkStrayThreads();
+
                 for (final IScheduledInvocationListener listener : mListeners) {
                     try {
                         listener.invocationComplete(mInvocationContext, deviceStates);
@@ -613,6 +618,30 @@
             }
         }
 
+        /** Check the number of thread in the ThreadGroup, only one should exists (itself). */
+        private void checkStrayThreads() {
+            int numThread = this.getThreadGroup().activeCount();
+            if (numThread == EXPECTED_THREAD_COUNT) {
+                // No stray thread detected at the end of invocation
+                return;
+            }
+            List<String> cmd = Arrays.asList(mCmd.getCommandTracker().getArgs());
+            CLog.e(
+                    "Stray thread detected for command %d, %s. %d threads instead of %d",
+                    mCmd.getCommandTracker().getId(), cmd, numThread, EXPECTED_THREAD_COUNT);
+            // This is the best we have for debug, it prints to std out.
+            this.getThreadGroup().list();
+            List<IHostMonitor> hostMonitors = GlobalConfiguration.getHostMonitorInstances();
+            if (hostMonitors != null) {
+                for (IHostMonitor hm : hostMonitors) {
+                    HostDataPoint data = new HostDataPoint("numThread", numThread, cmd.toString());
+                    hm.addHostEvent(HostMetricType.INVOCATION_STRAY_THREAD, data);
+                }
+            }
+            // printing to stderr will help to catch them.
+            System.err.println(String.format("We have %s threads instead of 1", numThread));
+        }
+
         /** Helper to log an invocation ended event. */
         private void logInvocationEndedEvent(
                 int invocId, long elapsedTime, final IInvocationContext context) {
@@ -1410,7 +1439,7 @@
         if (!isShuttingDown()) {
             CLog.d("initiating shutdown");
             removeAllCommands();
-            if (mReloadCmdfiles) {
+            if (mCommandFileWatcher != null) {
                 mCommandFileWatcher.cancel();
             }
             if (mCommandTimer != null) {
diff --git a/src/com/android/tradefed/config/Configuration.java b/src/com/android/tradefed/config/Configuration.java
index 64599ec..80e2aa3 100644
--- a/src/com/android/tradefed/config/Configuration.java
+++ b/src/com/android/tradefed/config/Configuration.java
@@ -1178,23 +1178,48 @@
         for (ISystemStatusChecker checker : getSystemStatusCheckers()) {
             dumpClassToXml(serializer, SYSTEM_STATUS_CHECKER_TYPE_NAME, checker);
         }
-        // TODO: fix device specific config object for multi device
-        dumpClassToXml(serializer, BUILD_PROVIDER_TYPE_NAME, getBuildProvider());
-        for (ITargetPreparer preparer : getTargetPreparers()) {
-            dumpClassToXml(serializer, TARGET_PREPARER_TYPE_NAME, preparer);
+
+        if (getDeviceConfig().size() > 1) {
+            // Handle multi device.
+            for (IDeviceConfiguration deviceConfig : getDeviceConfig()) {
+                serializer.startTag(null, Configuration.DEVICE_NAME);
+                serializer.attribute(null, "name", deviceConfig.getDeviceName());
+                dumpClassToXml(
+                        serializer, BUILD_PROVIDER_TYPE_NAME, deviceConfig.getBuildProvider());
+                for (ITargetPreparer preparer : deviceConfig.getTargetPreparers()) {
+                    dumpClassToXml(serializer, TARGET_PREPARER_TYPE_NAME, preparer);
+                }
+                dumpClassToXml(
+                        serializer, DEVICE_RECOVERY_TYPE_NAME, deviceConfig.getDeviceRecovery());
+                dumpClassToXml(
+                        serializer,
+                        DEVICE_REQUIREMENTS_TYPE_NAME,
+                        deviceConfig.getDeviceRequirements());
+                dumpClassToXml(
+                        serializer, DEVICE_OPTIONS_TYPE_NAME, deviceConfig.getDeviceOptions());
+                serializer.endTag(null, Configuration.DEVICE_NAME);
+            }
+        } else {
+            // Put single device tags
+            dumpClassToXml(serializer, BUILD_PROVIDER_TYPE_NAME, getBuildProvider());
+            for (ITargetPreparer preparer : getTargetPreparers()) {
+                dumpClassToXml(serializer, TARGET_PREPARER_TYPE_NAME, preparer);
+            }
+            dumpClassToXml(serializer, DEVICE_RECOVERY_TYPE_NAME, getDeviceRecovery());
+            dumpClassToXml(serializer, DEVICE_REQUIREMENTS_TYPE_NAME, getDeviceRequirements());
+            dumpClassToXml(serializer, DEVICE_OPTIONS_TYPE_NAME, getDeviceOptions());
         }
         for (IRemoteTest test : getTests()) {
             dumpClassToXml(serializer, TEST_TYPE_NAME, test);
         }
-        dumpClassToXml(serializer, DEVICE_RECOVERY_TYPE_NAME, getDeviceRecovery());
+
         dumpClassToXml(serializer, LOGGER_TYPE_NAME, getLogOutput());
         dumpClassToXml(serializer, LOG_SAVER_TYPE_NAME, getLogSaver());
         for (ITestInvocationListener listener : getTestInvocationListeners()) {
             dumpClassToXml(serializer, RESULT_REPORTER_TYPE_NAME, listener);
         }
         dumpClassToXml(serializer, CMD_OPTIONS_TYPE_NAME, getCommandOptions());
-        dumpClassToXml(serializer, DEVICE_REQUIREMENTS_TYPE_NAME, getDeviceRequirements());
-        dumpClassToXml(serializer, DEVICE_OPTIONS_TYPE_NAME, getDeviceOptions());
+
         dumpClassToXml(serializer, TEST_PROFILER_TYPE_NAME, getProfiler());
 
         serializer.endTag(null, CONFIGURATION_NAME);
diff --git a/src/com/android/tradefed/result/CollectingTestListener.java b/src/com/android/tradefed/result/CollectingTestListener.java
index e61f06a..1ca1cc1 100644
--- a/src/com/android/tradefed/result/CollectingTestListener.java
+++ b/src/com/android/tradefed/result/CollectingTestListener.java
@@ -299,10 +299,10 @@
     }
 
     /**
-     * Return total number of tests in a failure state (failed, assumption failure)
+     * Return total number of tests in a failure state (only failed, assumption failures do not
+     * count toward it).
      */
     public int getNumAllFailedTests() {
-        return getNumTestsInState(TestStatus.FAILURE) +
-                getNumTestsInState(TestStatus.ASSUMPTION_FAILURE);
+        return getNumTestsInState(TestStatus.FAILURE);
     }
 }
diff --git a/src/com/android/tradefed/suite/checker/SystemServerFileDescriptorChecker.java b/src/com/android/tradefed/suite/checker/SystemServerFileDescriptorChecker.java
new file mode 100644
index 0000000..9ed6d7b
--- /dev/null
+++ b/src/com/android/tradefed/suite/checker/SystemServerFileDescriptorChecker.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.suite.checker;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+
+/** Checks if system server appears to be running out of FDs. */
+public class SystemServerFileDescriptorChecker implements ISystemStatusChecker {
+    /** Process will fail to allocate beyond 1024, so heuristic considers 900 a bad state */
+    private static final int MAX_EXPECTED_FDS = 900;
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean postExecutionCheck(ITestDevice device) throws DeviceNotAvailableException {
+        Integer pid = getIntegerFromCommand(device, "pidof system_server");
+        if (pid == null) {
+            CLog.d("Unable to find system_server pid.");
+            return true;
+        }
+
+        Integer fds = getIntegerFromCommand(device, "ls /proc/" + pid + "/fd | wc -l");
+        if (fds == null) {
+            CLog.d("Unable to query system_server fd count.");
+            return true;
+        }
+
+        if (fds > MAX_EXPECTED_FDS) {
+            CLog.w("FDs currently allocated in system server " + fds);
+            return false;
+        }
+        return true;
+    }
+
+    private static Integer getIntegerFromCommand(ITestDevice device, String command)
+            throws DeviceNotAvailableException {
+        String output = device.executeShellCommand(command);
+        if (output == null) {
+            CLog.w("no shell output for command: " + command);
+            return null;
+        }
+        output = output.trim();
+        try {
+            return Integer.parseInt(output);
+        } catch (NumberFormatException e) {
+            CLog.w("unable to parse result of '" + command + "' : " + output);
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/tradefed/testtype/PythonUnitTestResultParser.java b/src/com/android/tradefed/testtype/PythonUnitTestResultParser.java
index 3ad8789..3b5b9dd 100644
--- a/src/com/android/tradefed/testtype/PythonUnitTestResultParser.java
+++ b/src/com/android/tradefed/testtype/PythonUnitTestResultParser.java
@@ -277,11 +277,10 @@
             // mark each test passed or failed
             for (Entry<TestIdentifier, String> test : mTestResultCache.entrySet()) {
                 listener.testStarted(test.getKey());
-                if (test.getValue() == null) {
-                    listener.testEnded(test.getKey(), Collections.<String, String>emptyMap());
-                } else {
+                if (test.getValue() != null) {
                     listener.testFailed(test.getKey(), test.getValue());
                 }
+                listener.testEnded(test.getKey(), Collections.<String, String>emptyMap());
             }
 
             // mark the whole run as passed or failed
diff --git a/src/com/android/tradefed/testtype/VersionedTfLauncher.java b/src/com/android/tradefed/testtype/VersionedTfLauncher.java
index f7f9641..1b5b5fb 100644
--- a/src/com/android/tradefed/testtype/VersionedTfLauncher.java
+++ b/src/com/android/tradefed/testtype/VersionedTfLauncher.java
@@ -21,9 +21,10 @@
 import com.android.tradefed.config.OptionCopier;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.NullDevice;
-import com.android.tradefed.util.QuotationAwareTokenizer;
+import com.android.tradefed.util.StringEscapeUtils;
 
-import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -35,9 +36,9 @@
 public class VersionedTfLauncher extends SubprocessTfLauncher
         implements IMultiDeviceTest, IStrictShardableTest {
 
-    @Option(name = "tf-command-line", description = "The string of original command line "
+    @Option(name = "tf-command-line", description = "The list string of original command line "
             + "arguments.")
-    private String mTfCommandline = null;
+    private List<String> mTfCommandline = new ArrayList<>();
 
     private Map<ITestDevice, IBuildInfo> mDeviceInfos = null;
 
@@ -67,8 +68,8 @@
     protected void preRun() {
         super.preRun();
 
-        if (mTfCommandline != null) {
-            mCmdArgs.addAll(Arrays.asList(QuotationAwareTokenizer.tokenizeLine(mTfCommandline)));
+        if (!mTfCommandline.isEmpty()) {
+            mCmdArgs.addAll(StringEscapeUtils.paramsToArgs(mTfCommandline));
         }
 
         // TODO: support multiple device test.
diff --git a/src/com/android/tradefed/util/DeviceRecoveryModeUtil.java b/src/com/android/tradefed/util/DeviceRecoveryModeUtil.java
new file mode 100644
index 0000000..0441fd3
--- /dev/null
+++ b/src/com/android/tradefed/util/DeviceRecoveryModeUtil.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.TimeoutException;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.IManagedTestDevice;
+import com.android.tradefed.device.TestDeviceState;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.io.IOException;
+
+public class DeviceRecoveryModeUtil {
+
+    /**
+     * Boots a device in recovery mode back into the main OS.
+     *
+     * @param device the {@link IManagedTestDevice} to boot
+     * @param timeoutMs how long to wait for the device to be in the recovery state
+     * @throws DeviceNotAvailableException
+     */
+    public static void bootOutOfRecovery(IManagedTestDevice device, long timeoutMs)
+            throws DeviceNotAvailableException {
+        bootOutOfRecovery(device, timeoutMs, 0);
+    }
+
+    /**
+     * Boots a device in recovery mode back into the main OS. Can optionally wait after the
+     * RECOVERY state begins, as it may not be immediately possible to send adb commands when
+     * recovery starts.
+     *
+     * @param managedDevice the {@link IManagedTestDevice} to boot
+     * @param timeoutMs how long to wait for the device to be in the recovery state
+     * @param bufferMs the number of ms to wait after the device enters the recovery state
+     * @throws DeviceNotAvailableException
+     */
+    public static void bootOutOfRecovery(IManagedTestDevice managedDevice,
+            long timeoutMs, long bufferMs)
+            throws DeviceNotAvailableException {
+        if (managedDevice.getDeviceState().equals(TestDeviceState.RECOVERY)) {
+            CLog.i("Rebooting to exit recovery");
+            if (bufferMs > 0) {
+                CLog.i("Pausing for %d ms while recovery loads", bufferMs);
+                RunUtil.getDefault().sleep(bufferMs);
+            }
+            try {
+                // we don't want to enable root until the reboot is fully finished and
+                // the device is available, or it may get stuck in recovery and time out
+                managedDevice.getIDevice().reboot(null);
+                managedDevice.waitForDeviceAvailable(timeoutMs);
+                managedDevice.postBootSetup();
+            } catch (TimeoutException | AdbCommandRejectedException | IOException e) {
+                CLog.e("Failed to reboot, trying last-ditch recovery");
+                CLog.e(e);
+                managedDevice.recoverDevice();
+            }
+        }
+    }
+}
diff --git a/src/com/android/tradefed/util/StringEscapeUtils.java b/src/com/android/tradefed/util/StringEscapeUtils.java
index bbb0aab..1b7ea44 100644
--- a/src/com/android/tradefed/util/StringEscapeUtils.java
+++ b/src/com/android/tradefed/util/StringEscapeUtils.java
@@ -16,6 +16,9 @@
 
 package com.android.tradefed.util;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 
 /**
  * Utility class for escaping strings for specific formats.
@@ -51,4 +54,31 @@
         }
         return out.toString();
     }
+
+    /**
+     * Converts the provided parameters via options to command line args to sub process
+     *
+     * <p>This method will do a simplistic generic unescape for each parameter in the list. It
+     * replaces \[char] with [char]. For example, \" is converted to ". This allows string with
+     * escaped double quotes to stay as a string after being parsed by QuotationAwareTokenizer.
+     * Without this QuotationAwareTokenizer will break the string into sections if it has space in
+     * it.
+     *
+     * @param params parameters received via options
+     * @return list of string representing command line args
+     */
+    public static List<String> paramsToArgs(List<String> params) {
+        List<String> result = new ArrayList<>();
+        for (String param : params) {
+            // doing a simplistic generic unescape here: \<char> is replaced with <char>; note that
+            // this may lead to incorrect results such as \n -> n, or \t -> t, but it's unclear why
+            // a command line param would have \t anyways
+            param = param.replaceAll("\\\\(.)", "$1");
+            String[] args = QuotationAwareTokenizer.tokenizeLine(param);
+            if (args.length != 0) {
+                result.addAll(Arrays.asList(args));
+            }
+        }
+        return result;
+    }
 }
diff --git a/src/com/android/tradefed/util/hostmetric/AbstractHostMonitor.java b/src/com/android/tradefed/util/hostmetric/AbstractHostMonitor.java
index de72c0d..83cb8e0 100644
--- a/src/com/android/tradefed/util/hostmetric/AbstractHostMonitor.java
+++ b/src/com/android/tradefed/util/hostmetric/AbstractHostMonitor.java
@@ -45,7 +45,7 @@
     @Option(name = "event-tag", description = "Event Tag that will be accepted by the Monitor.")
     private HostMetricType mTag = HostMetricType.NONE;
 
-    protected Queue<DataPoint> mHostEvents = new LinkedBlockingQueue<DataPoint>();
+    protected Queue<HostDataPoint> mHostEvents = new LinkedBlockingQueue<HostDataPoint>();
 
     protected Map<String, String> mHostData = new HashMap<>();
 
@@ -83,7 +83,7 @@
 
     /** {@inheritDoc} */
     @Override
-    public synchronized void addHostEvent(HostMetricType tag, DataPoint event) {
+    public synchronized void addHostEvent(HostMetricType tag, HostDataPoint event) {
         if (getTag().equals(tag)) {
             mHostEvents.add(event);
         }
diff --git a/src/com/android/tradefed/util/hostmetric/IHostMonitor.java b/src/com/android/tradefed/util/hostmetric/IHostMonitor.java
index 504b420..20a4d3b 100644
--- a/src/com/android/tradefed/util/hostmetric/IHostMonitor.java
+++ b/src/com/android/tradefed/util/hostmetric/IHostMonitor.java
@@ -31,27 +31,25 @@
     public void start();
 
     /** A method that will be called to add a special event to be sent. */
-    public void addHostEvent(HostMetricType tag, DataPoint event);
+    public void addHostEvent(HostMetricType tag, HostDataPoint event);
 
     /**
      * A method that will be called to stop the Host Monitor.
      */
     public void terminate();
 
-    /**
-     * Generic class for data to be reported.
-     */
-    static class DataPoint {
+    /** Generic class for data to be reported. */
+    public static class HostDataPoint {
         public String name;
         public int value;
         public String additionalInfo = null;
 
-        public DataPoint(String name, int value) {
+        public HostDataPoint(String name, int value) {
             this.name = name;
             this.value = value;
         }
 
-        public DataPoint(String name, int value, String additionalInfo) {
+        public HostDataPoint(String name, int value, String additionalInfo) {
             this.name = name;
             this.value = value;
             this.additionalInfo = additionalInfo;
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index c98e1fc..2efc216 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -90,6 +90,7 @@
 import com.android.tradefed.result.TestSummaryTest;
 import com.android.tradefed.result.XmlResultReporterTest;
 import com.android.tradefed.suite.checker.KeyguardStatusCheckerTest;
+import com.android.tradefed.suite.checker.SystemServerFileDescriptorCheckerTest;
 import com.android.tradefed.suite.checker.SystemServerStatusCheckerTest;
 import com.android.tradefed.targetprep.AllTestAppsInstallSetupTest;
 import com.android.tradefed.targetprep.AppSetupTest;
@@ -326,6 +327,7 @@
 
     // suite/checker
     KeyguardStatusCheckerTest.class,
+    SystemServerFileDescriptorCheckerTest.class,
     SystemServerStatusCheckerTest.class,
 
     // testtype
diff --git a/tests/src/com/android/tradefed/command/CommandRunnerTest.java b/tests/src/com/android/tradefed/command/CommandRunnerTest.java
index 7960516..f724193 100644
--- a/tests/src/com/android/tradefed/command/CommandRunnerTest.java
+++ b/tests/src/com/android/tradefed/command/CommandRunnerTest.java
@@ -33,7 +33,9 @@
 import com.android.tradefed.util.FileUtil;
 
 import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -56,7 +58,23 @@
     private Throwable mThrowable = null;
     private String mStackTraceOutput = null;
 
-    private ICommandScheduler mOriginalScheduler = null;
+    private static ICommandScheduler sOriginalScheduler = null;
+
+    @BeforeClass
+    public static void beforeClass() throws Exception {
+        // We have some global state that cannot be re-entered so we ensure they do not throw.
+        try {
+            GlobalConfiguration.createGlobalConfiguration(new String[] {});
+        } catch (IllegalStateException e) {
+            // ignore re-init.
+        }
+        sOriginalScheduler = GlobalConfiguration.getInstance().getCommandScheduler();
+    }
+
+    @AfterClass
+    public static void afterClass() {
+        GlobalConfiguration.getInstance().setCommandScheduler(sOriginalScheduler);
+    }
 
     @Before
     public void setUp() {
@@ -66,15 +84,6 @@
                 new CommandRunner() {
                     @Override
                     public void initGlobalConfig(String[] args) throws ConfigurationException {
-                        // We have some global state that cannot be re-entered so we ensure they do
-                        // not throw.
-                        try {
-                            GlobalConfiguration.createGlobalConfiguration(args);
-                        } catch (IllegalStateException e) {
-                            // ignore re-init.
-                        }
-                        mOriginalScheduler =
-                                GlobalConfiguration.getInstance().getCommandScheduler();
                         GlobalConfiguration.getInstance()
                                 .setCommandScheduler(
                                         new CommandScheduler() {
@@ -130,7 +139,9 @@
 
     @After
     public void tearDown() {
-        GlobalConfiguration.getInstance().setCommandScheduler(mOriginalScheduler);
+        GlobalConfiguration.getInstance()
+                .getCommandScheduler()
+                .setLastInvocationExitCode(ExitCode.NO_ERROR, null);
     }
 
     /**
diff --git a/tests/src/com/android/tradefed/config/ConfigurationTest.java b/tests/src/com/android/tradefed/config/ConfigurationTest.java
index 09014c0..d16d5d4 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationTest.java
@@ -641,4 +641,27 @@
             FileUtil.deleteFile(test);
         }
     }
+
+    /**
+     * Test that {@link Configuration#dumpXml(PrintWriter)} produce the xml output even for a multi
+     * device situation.
+     */
+    public void testDumpXml_multi_device() throws Exception {
+        List<IDeviceConfiguration> deviceObjectList = new ArrayList<IDeviceConfiguration>();
+        deviceObjectList.add(new DeviceConfigurationHolder("device1"));
+        deviceObjectList.add(new DeviceConfigurationHolder("device2"));
+        mConfig.setConfigurationObjectList(Configuration.DEVICE_NAME, deviceObjectList);
+        File test = FileUtil.createTempFile("dumpxml", "xml");
+        try {
+            PrintWriter out = new PrintWriter(test);
+            mConfig.dumpXml(out);
+            out.flush();
+            String content = FileUtil.readStringFromFile(test);
+            assertTrue(content.length() > 100);
+            assertTrue(content.contains("<device name=\"device1\">"));
+            assertTrue(content.contains("<device name=\"device2\">"));
+        } finally {
+            FileUtil.deleteFile(test);
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/suite/checker/SystemServerFileDescriptorCheckerTest.java b/tests/src/com/android/tradefed/suite/checker/SystemServerFileDescriptorCheckerTest.java
new file mode 100644
index 0000000..62f0f64
--- /dev/null
+++ b/tests/src/com/android/tradefed/suite/checker/SystemServerFileDescriptorCheckerTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.suite.checker;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.device.ITestDevice;
+
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SystemServerFileDescriptorChecker} */
+@RunWith(JUnit4.class)
+public class SystemServerFileDescriptorCheckerTest {
+
+    private SystemServerFileDescriptorChecker mChecker;
+    private ITestDevice mMockDevice;
+
+    @Before
+    public void setUp() {
+        mMockDevice = EasyMock.createMock(ITestDevice.class);
+        mChecker = new SystemServerFileDescriptorChecker();
+    }
+
+    @Test
+    public void testFailToGetPid() throws Exception {
+        EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
+                .andReturn("not found\n");
+        EasyMock.replay(mMockDevice);
+        assertTrue(mChecker.preExecutionCheck(mMockDevice));
+        assertTrue(mChecker.postExecutionCheck(mMockDevice));
+        EasyMock.verify(mMockDevice);
+    }
+
+    @Test
+    public void testFailToGetFdCount() throws Exception {
+        EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
+                .andReturn("1024\n");
+        EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("ls /proc/1024/fd | wc -l")))
+                .andReturn("not found\n");
+        EasyMock.replay(mMockDevice);
+        assertTrue(mChecker.preExecutionCheck(mMockDevice));
+        assertTrue(mChecker.postExecutionCheck(mMockDevice));
+        EasyMock.verify(mMockDevice);
+    }
+
+    @Test
+    public void testAcceptableFdCount() throws Exception {
+        EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
+                .andReturn("914\n");
+        EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("ls /proc/914/fd | wc -l")))
+                .andReturn("382  \n");
+        EasyMock.replay(mMockDevice);
+        assertTrue(mChecker.preExecutionCheck(mMockDevice));
+        assertTrue(mChecker.postExecutionCheck(mMockDevice));
+        EasyMock.verify(mMockDevice);
+    }
+
+    @Test
+    public void testUnacceptableFdCount() throws Exception {
+        EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
+                .andReturn("914\n");
+        EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("ls /proc/914/fd | wc -l")))
+                .andReturn("1002  \n");
+        EasyMock.replay(mMockDevice);
+        assertTrue(mChecker.preExecutionCheck(mMockDevice)); // Noop
+        assertFalse(mChecker.postExecutionCheck(mMockDevice));
+        EasyMock.verify(mMockDevice);
+    }
+}
diff --git a/tests/src/com/android/tradefed/testtype/PythonUnitTestResultParserTest.java b/tests/src/com/android/tradefed/testtype/PythonUnitTestResultParserTest.java
index 79908a6..8afdfd1 100644
--- a/tests/src/com/android/tradefed/testtype/PythonUnitTestResultParserTest.java
+++ b/tests/src/com/android/tradefed/testtype/PythonUnitTestResultParserTest.java
@@ -234,7 +234,7 @@
                 mMockListener.testFailed(eq(ids[i]), (String)anyObject());
                 expectLastCall().times(1);
                 mMockListener.testEnded(ids[i], Collections.<String, String>emptyMap());
-                expectLastCall().andThrow(new AssertionFailedError()).anyTimes();
+                expectLastCall().times(1);
             }
         }
     }
diff --git a/tests/src/com/android/tradefed/testtype/VersionedTfLauncherTest.java b/tests/src/com/android/tradefed/testtype/VersionedTfLauncherTest.java
index 32f5e6c..5a647f7 100644
--- a/tests/src/com/android/tradefed/testtype/VersionedTfLauncherTest.java
+++ b/tests/src/com/android/tradefed/testtype/VersionedTfLauncherTest.java
@@ -50,8 +50,14 @@
     private static final String CONFIG_NAME = "FAKE_CONFIG";
     private static final String TF_COMMAND_LINE_TEMPLATE = "--template:map";
     private static final String TF_COMMAND_LINE_TEST = "test=tf/fake";
+    // Test option value with empty spaces should be parsed correctly.
+    private static final String TF_COMMAND_LINE_OPTION = "--option";
+    private static final String TF_COMMAND_LINE_OPTION_VALUE = "value1 value2";
+    private static final String TF_COMMAND_LINE_OPTION_VALUE_QUOTED =
+            ("\\\"" + TF_COMMAND_LINE_OPTION_VALUE + "\\\"");
     private static final String TF_COMMAND_LINE =
-            (TF_COMMAND_LINE_TEMPLATE + " " + TF_COMMAND_LINE_TEST);
+            (TF_COMMAND_LINE_TEMPLATE + " " + TF_COMMAND_LINE_TEST + " " + TF_COMMAND_LINE_OPTION +
+             " " + TF_COMMAND_LINE_OPTION_VALUE_QUOTED);
 
     private VersionedTfLauncher mVersionedTfLauncher;
     private ITestInvocationListener mMockListener;
@@ -100,6 +106,8 @@
                                 EasyMock.eq(CONFIG_NAME),
                                 EasyMock.eq(TF_COMMAND_LINE_TEMPLATE),
                                 EasyMock.eq(TF_COMMAND_LINE_TEST),
+                                EasyMock.eq(TF_COMMAND_LINE_OPTION),
+                                EasyMock.eq(TF_COMMAND_LINE_OPTION_VALUE),
                                 EasyMock.eq("--serial"),
                                 EasyMock.eq(FAKE_SERIAL),
                                 EasyMock.eq("--subprocess-report-file"),
@@ -149,6 +157,8 @@
                                 EasyMock.eq(CONFIG_NAME),
                                 EasyMock.eq(TF_COMMAND_LINE_TEMPLATE),
                                 EasyMock.eq(TF_COMMAND_LINE_TEST),
+                                EasyMock.eq(TF_COMMAND_LINE_OPTION),
+                                EasyMock.eq(TF_COMMAND_LINE_OPTION_VALUE),
                                 EasyMock.eq("--null-device"),
                                 EasyMock.eq("--subprocess-report-file"),
                                 (String) EasyMock.anyObject()))
@@ -206,6 +216,8 @@
                                 EasyMock.eq(CONFIG_NAME),
                                 EasyMock.eq(TF_COMMAND_LINE_TEMPLATE),
                                 EasyMock.eq(TF_COMMAND_LINE_TEST),
+                                EasyMock.eq(TF_COMMAND_LINE_OPTION),
+                                EasyMock.eq(TF_COMMAND_LINE_OPTION_VALUE),
                                 EasyMock.eq("--serial"),
                                 EasyMock.eq(FAKE_SERIAL),
                                 EasyMock.eq("--shard-count"),
diff --git a/tests/src/com/android/tradefed/util/DeviceRecoveryModeUtilTest.java b/tests/src/com/android/tradefed/util/DeviceRecoveryModeUtilTest.java
new file mode 100644
index 0000000..7e923d9
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/DeviceRecoveryModeUtilTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util;
+
+import com.android.ddmlib.IDevice;
+import com.android.tradefed.device.IManagedTestDevice;
+import com.android.tradefed.device.TestDeviceState;
+
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class DeviceRecoveryModeUtilTest {
+
+    private IManagedTestDevice mMockManagedDevice;
+    private IDevice mMockDevice;
+
+    @Before
+    public void setUp() throws Throwable {
+        mMockManagedDevice = EasyMock.createMock(IManagedTestDevice.class);
+        mMockDevice = EasyMock.createMock(IDevice.class);
+        EasyMock.expect(mMockManagedDevice.getIDevice()).andReturn(mMockDevice).anyTimes();
+    }
+
+    @Test
+    public void testBootOutOfRecovery() throws Throwable {
+        mMockDevice.reboot((String) EasyMock.anyObject());
+        EasyMock.expectLastCall();
+        mMockManagedDevice.waitForDeviceAvailable(EasyMock.anyLong());
+        EasyMock.expectLastCall();
+        mMockManagedDevice.postBootSetup();
+        EasyMock.expectLastCall();
+        EasyMock.expect(mMockManagedDevice.getDeviceState()).andReturn(TestDeviceState.RECOVERY);
+        EasyMock.replay(mMockDevice, mMockManagedDevice);
+        DeviceRecoveryModeUtil.bootOutOfRecovery(mMockManagedDevice, 1000000000);
+    }
+}
diff --git a/tests/src/com/android/tradefed/util/StringEscapeUtilsTest.java b/tests/src/com/android/tradefed/util/StringEscapeUtilsTest.java
index a1e104f..2001274 100644
--- a/tests/src/com/android/tradefed/util/StringEscapeUtilsTest.java
+++ b/tests/src/com/android/tradefed/util/StringEscapeUtilsTest.java
@@ -15,17 +15,77 @@
  */
 package com.android.tradefed.util;
 
-import junit.framework.TestCase;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertArrayEquals;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Unit tests for {@link StringEscapeUtils}
  */
-public class StringEscapeUtilsTest extends TestCase {
+@RunWith(JUnit4.class)
+public class StringEscapeUtilsTest {
     /**
      * Simple test that {@link StringEscapeUtils#escapeShell(String)} escapes the dollar sign.
      */
+    @Test
     public void testEscapesDollarSigns() {
         String escaped_str = StringEscapeUtils.escapeShell("$money$signs");
         assertEquals("\\$money\\$signs", escaped_str);
     }
+
+    /**
+     * Test {@link StringEscapeUtils#paramsToArgs(List) returns proper result with no quoting
+     * or spaces
+     */
+    @Test
+    public void testParams_noQuotesNoSpaces() {
+        List<String> expected = new ArrayList<>();
+        expected.add("foo");
+        expected.add("bar");
+        assertArrayEquals(expected.toArray(),
+                StringEscapeUtils.paramsToArgs(expected).toArray());
+    }
+
+    /**
+     * Test {@link StringEscapeUtils#paramsToArgs(List) returns proper result with no quoting
+     * but with spaces
+     */
+    @Test
+    public void testParams_noQuotesWithSpaces() {
+        List<String> expected = new ArrayList<>();
+        expected.add("foo");
+        expected.add("bar bar");
+        assertArrayEquals(new String[]{"foo", "bar", "bar"},
+                StringEscapeUtils.paramsToArgs(expected).toArray());
+    }
+
+    /**
+     * Test {@link StringEscapeUtils#paramsToArgs(List) returns proper result with plain quoting
+     */
+    @Test
+    public void testParams_plainQuotes() {
+        List<String> expected = new ArrayList<>();
+        expected.add("foo");
+        expected.add("\"bar bar\"");
+        assertArrayEquals(new String[]{"foo", "bar bar"},
+                StringEscapeUtils.paramsToArgs(expected).toArray());
+    }
+
+    /**
+     * Test {@link StringEscapeUtils#paramsToArgs(List) returns proper result with escaped quoting
+     */
+    @Test
+    public void testParams_escapedQuotes() {
+        List<String> expected = new ArrayList<>();
+        expected.add("foo");
+        expected.add("\\\"bar bar\\\"");
+        assertArrayEquals(new String[]{"foo", "bar bar"},
+                StringEscapeUtils.paramsToArgs(expected).toArray());
+    }
 }
diff --git a/tests/src/com/android/tradefed/util/hostmetric/AbstractHostMonitorTest.java b/tests/src/com/android/tradefed/util/hostmetric/AbstractHostMonitorTest.java
index ad3ce56..b7046cb 100644
--- a/tests/src/com/android/tradefed/util/hostmetric/AbstractHostMonitorTest.java
+++ b/tests/src/com/android/tradefed/util/hostmetric/AbstractHostMonitorTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.tradefed.util.hostmetric;
 
+import com.android.tradefed.util.hostmetric.IHostMonitor.HostDataPoint;
 import com.android.tradefed.util.hostmetric.IHostMonitor.HostMetricType;
 
 import junit.framework.TestCase;
@@ -38,12 +39,12 @@
     }
 
     /**
-     * Test {@link AbstractHostMonitor#addHostEvent(HostMetricType, IHostMonitor.DataPoint)} when
-     * the event is properly added.
+     * Test {@link AbstractHostMonitor#addHostEvent(HostMetricType, HostDataPoint)} when the event
+     * is properly added.
      */
     public void testaddHostEvent() {
         assertTrue(mHostMonitor.getQueueSize() == 0);
-        IHostMonitor.DataPoint fakeDataPoint = new IHostMonitor.DataPoint("test", 5);
+        HostDataPoint fakeDataPoint = new HostDataPoint("test", 5);
         mHostMonitor.addHostEvent(mHostMonitor.getTag(), fakeDataPoint);
         assertTrue(mHostMonitor.getQueueSize() == 1);
         mHostMonitor.addHostEvent(mHostMonitor.getTag(), fakeDataPoint);
@@ -51,12 +52,12 @@
     }
 
     /**
-     * Test {@link AbstractHostMonitor#addHostEvent(HostMetricType, IHostMonitor.DataPoint)} when
-     * the event has a different tag than the Monitor, it should not be added.
+     * Test {@link AbstractHostMonitor#addHostEvent(HostMetricType, HostDataPoint)} when the event
+     * has a different tag than the Monitor, it should not be added.
      */
     public void testaddHostEvent_differentTag() {
         assertTrue(mHostMonitor.getQueueSize() == 0);
-        IHostMonitor.DataPoint fakeDataPoint = new IHostMonitor.DataPoint("test", 5);
+        HostDataPoint fakeDataPoint = new HostDataPoint("test", 5);
         // expected NONE key for hostmonitor
         mHostMonitor.addHostEvent(HostMetricType.INVOCATION_STRAY_THREAD, fakeDataPoint);
         assertTrue(mHostMonitor.getQueueSize() == 0);