Merge "Search the parameterized portion of test name non-greedy"
diff --git a/clearcut_client/com/android/tradefed/clearcut/ClearcutClient.java b/clearcut_client/com/android/tradefed/clearcut/ClearcutClient.java
index ee1d573..85adbbb 100644
--- a/clearcut_client/com/android/tradefed/clearcut/ClearcutClient.java
+++ b/clearcut_client/com/android/tradefed/clearcut/ClearcutClient.java
@@ -140,6 +140,20 @@
         queueEvent(request.build());
     }
 
+    /** Send the event to notify that a Tradefed invocation was started. */
+    public void notifyTradefedInvocationStartEvent() {
+        if (mDisabled) {
+            return;
+        }
+        LogRequest.Builder request = createBaseLogRequest();
+        LogEvent.Builder logEvent = LogEvent.newBuilder();
+        logEvent.setEventTimeMs(System.currentTimeMillis());
+        logEvent.setSourceExtension(
+                ClearcutEventHelper.createRunStartEvent(getGroupingKey(), mRunId, mUserType));
+        request.addLogEvent(logEvent);
+        queueEvent(request.build());
+    }
+
     /** Stop the periodic sending of clearcut events */
     public void stop() {
         if (mExecutor != null) {
diff --git a/clearcut_client/com/android/tradefed/clearcut/ClearcutEventHelper.java b/clearcut_client/com/android/tradefed/clearcut/ClearcutEventHelper.java
index e8a6ab3..23ad99e 100644
--- a/clearcut_client/com/android/tradefed/clearcut/ClearcutEventHelper.java
+++ b/clearcut_client/com/android/tradefed/clearcut/ClearcutEventHelper.java
@@ -18,6 +18,7 @@
 import com.android.asuite.clearcut.Common.UserType;
 import com.android.asuite.clearcut.ExternalUserLog.AtestLogEventExternal;
 import com.android.asuite.clearcut.ExternalUserLog.AtestLogEventExternal.AtestStartEvent;
+import com.android.asuite.clearcut.ExternalUserLog.AtestLogEventExternal.RunnerFinishEvent;
 import com.android.asuite.clearcut.InternalUserLog.AtestLogEventInternal;
 
 import com.google.protobuf.ByteString;
@@ -53,6 +54,31 @@
     }
 
     /**
+     * Create the start invocation event for Tradefed.
+     *
+     * @param userKey The unique id representing the user
+     * @param runId The current id for the session.
+     * @param userType The type of the user: internal or external.
+     * @return a ByteString representation of the even proto.
+     */
+    public static ByteString createRunStartEvent(String userKey, String runId, UserType userType) {
+        if (UserType.GOOGLE.equals(userType)) {
+            AtestLogEventInternal.Builder builder =
+                    createBaseInternalEventBuilder(userKey, runId, userType);
+            AtestLogEventInternal.RunnerFinishEvent.Builder startRunEventBuilder =
+                    AtestLogEventInternal.RunnerFinishEvent.newBuilder();
+            builder.setRunnerFinishEvent(startRunEventBuilder.build());
+            return builder.build().toByteString();
+        }
+
+        AtestLogEventExternal.Builder builder =
+                createBaseExternalEventBuilder(userKey, runId, userType);
+        RunnerFinishEvent.Builder startBuilder = RunnerFinishEvent.newBuilder();
+        builder.setRunnerFinishEvent(startBuilder.build());
+        return builder.build().toByteString();
+    }
+
+    /**
      * Create the basic event builder with all the common informations.
      *
      * @param userKey The unique id representing the user
diff --git a/common_util/com/android/tradefed/result/LogDataType.java b/common_util/com/android/tradefed/result/LogDataType.java
index 9547659..da1eb9e 100644
--- a/common_util/com/android/tradefed/result/LogDataType.java
+++ b/common_util/com/android/tradefed/result/LogDataType.java
@@ -38,6 +38,7 @@
     CLANG_COVERAGE("profdata", "text/plain", false, false), // LLVM indexed profile data
     PB("pb", "application/octet-stream", true, false), // Binary proto file
     TEXTPB("textproto", "text/plain", false, true), // Text proto file
+    PERFETTO("pb", "application/octet-stream", true, false), // binary proto perfetto trace file
     /* Specific text file types */
     BUGREPORT("txt", "text/plain", false, true),
     BUGREPORTZ("zip", "application/zip", true, false),
diff --git a/common_util/com/android/tradefed/util/IRunUtil.java b/common_util/com/android/tradefed/util/IRunUtil.java
index 3f42581..b4df9d6 100644
--- a/common_util/com/android/tradefed/util/IRunUtil.java
+++ b/common_util/com/android/tradefed/util/IRunUtil.java
@@ -51,6 +51,11 @@
         public default List<String> getCommand() {
             return null;
         }
+
+        /** Returns the {@link CommandResult} associated with the command. */
+        public default CommandResult getResult() {
+            return null;
+        }
     }
 
     /**
diff --git a/common_util/com/android/tradefed/util/RunUtil.java b/common_util/com/android/tradefed/util/RunUtil.java
index 412a9c4..5eb6c31 100644
--- a/common_util/com/android/tradefed/util/RunUtil.java
+++ b/common_util/com/android/tradefed/util/RunUtil.java
@@ -21,6 +21,7 @@
 import com.android.tradefed.log.LogUtil.CLog;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
 
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayOutputStream;
@@ -565,12 +566,14 @@
             } catch (InterruptedException e) {
                 CLog.i("runutil interrupted");
                 status = CommandStatus.EXCEPTION;
+                backFillException(mRunnable.getResult(), e);
             } catch (Exception e) {
                 if (mLogErrors) {
                     CLog.e("Exception occurred when executing runnable");
                     CLog.e(e);
                 }
                 status = CommandStatus.EXCEPTION;
+                backFillException(mRunnable.getResult(), e);
             }
             synchronized (this) {
                 mStatus = status;
@@ -584,6 +587,15 @@
         synchronized CommandStatus getStatus() {
             return mStatus;
         }
+
+        private void backFillException(CommandResult result, Exception e) {
+            if (result == null) {
+                return;
+            }
+            if (Strings.isNullOrEmpty(result.getStderr())) {
+                result.setStderr(StreamUtil.getStackTrace(e));
+            }
+        }
     }
 
     class RunnableResult implements IRunUtil.IRunnableResult {
@@ -651,6 +663,7 @@
             return new ArrayList<>(mProcessBuilder.command());
         }
 
+        @Override
         public CommandResult getResult() {
             return mCommandResult;
         }
diff --git a/src/com/android/tradefed/command/CommandScheduler.java b/src/com/android/tradefed/command/CommandScheduler.java
index 64636a1..e3250d8 100644
--- a/src/com/android/tradefed/command/CommandScheduler.java
+++ b/src/com/android/tradefed/command/CommandScheduler.java
@@ -562,6 +562,9 @@
 
         @Override
         public void run() {
+            if (mClient != null) {
+                mClient.notifyTradefedInvocationStartEvent();
+            }
             mStartTime = System.currentTimeMillis();
             ITestInvocation instance = getInvocation();
             IConfiguration config = mCmd.getConfiguration();
diff --git a/src/com/android/tradefed/device/DeviceManager.java b/src/com/android/tradefed/device/DeviceManager.java
index 7e08c4f..a653c1e 100644
--- a/src/com/android/tradefed/device/DeviceManager.java
+++ b/src/com/android/tradefed/device/DeviceManager.java
@@ -916,7 +916,7 @@
      * @return std output if the command succeedm null otherwise.
      */
     public String executeGlobalAdbCommand(String... cmdArgs) {
-        String[] fullCmd = ArrayUtil.buildArray(new String[] {"adb"}, cmdArgs);
+        String[] fullCmd = ArrayUtil.buildArray(new String[] {getAdbPath()}, cmdArgs);
         CommandResult result = getRunUtil().runTimedCmd(FASTBOOT_CMD_TIMEOUT, fullCmd);
         if (CommandStatus.SUCCESS.equals(result.getStatus())) {
             return result.getStdout();
diff --git a/src/com/android/tradefed/device/cloud/GceManager.java b/src/com/android/tradefed/device/cloud/GceManager.java
index 3e4ea4e..3e6a77d 100644
--- a/src/com/android/tradefed/device/cloud/GceManager.java
+++ b/src/com/android/tradefed/device/cloud/GceManager.java
@@ -474,18 +474,15 @@
         if (gceAvd == null || gceAvd.hostAndPort() == null) {
             return null;
         }
-        String output = "";
-        // Retry a couple of time because adb might not be started for that user.
-        // FIXME: See if we can use vsoc-01 directly to avoid this
-        for (int i = 0; i < 3; i++) {
-            output =
-                    remoteSshCommandExec(
-                            gceAvd, options, runUtil, "./bin/adb", "shell", "bugreportz");
-            Matcher match = BUGREPORTZ_RESPONSE_PATTERN.matcher(output);
-            if (match.find()) {
-                break;
-            }
-        }
+        String output =
+                remoteSshCommandExec(
+                        gceAvd,
+                        options,
+                        runUtil,
+                        "./bin/adb",
+                        "wait-for-device",
+                        "shell",
+                        "bugreportz");
         Matcher match = BUGREPORTZ_RESPONSE_PATTERN.matcher(output);
         if (!match.find()) {
             CLog.e("Something went wrong during bugreportz collection: '%s'", output);
diff --git a/src/com/android/tradefed/invoker/TestInvocation.java b/src/com/android/tradefed/invoker/TestInvocation.java
index fdd0a4b..0efc7a4 100644
--- a/src/com/android/tradefed/invoker/TestInvocation.java
+++ b/src/com/android/tradefed/invoker/TestInvocation.java
@@ -26,6 +26,7 @@
 import com.android.tradefed.config.DynamicRemoteFileResolver;
 import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.device.DeviceManager;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.DeviceUnresponsiveException;
 import com.android.tradefed.device.FreeDeviceState;
@@ -1059,6 +1060,12 @@
         }
         if (countPhysicalLost > 0) {
             addInvocationMetric(InvocationMetricKey.DEVICE_LOST_DETECTED, countPhysicalLost);
+            if (GlobalConfiguration.getDeviceManagerInstance() instanceof DeviceManager) {
+                String adbOutput =
+                        ((DeviceManager) GlobalConfiguration.getDeviceManagerInstance())
+                                .executeGlobalAdbCommand("devices");
+                CLog.e("'adb devices' output:\n%s", adbOutput);
+            }
         }
         return devicesStates;
     }
diff --git a/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java b/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
index 1c68f50..257e9e5 100644
--- a/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
+++ b/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
@@ -278,7 +278,7 @@
                 }
 
             } else {
-                testLog(metricFile.getName(), LogDataType.PB, source);
+                testLog(metricFile.getName(), LogDataType.PERFETTO, source);
             }
         }
 
diff --git a/test_framework/com/android/tradefed/testtype/GTest.java b/test_framework/com/android/tradefed/testtype/GTest.java
index 444a278..41c5dac 100644
--- a/test_framework/com/android/tradefed/testtype/GTest.java
+++ b/test_framework/com/android/tradefed/testtype/GTest.java
@@ -428,6 +428,7 @@
         // Insert the coverage listener if code coverage collection is enabled.
         listener = addNativeCoverageListenerIfEnabled(listener);
         listener = addClangCoverageListenerIfEnabled(listener);
+        listener = getGTestListener(listener);
         NativeCodeCoverageFlusher flusher =
                 new NativeCodeCoverageFlusher(mDevice, getCoverageOptions().getCoverageProcesses());
 
diff --git a/test_framework/com/android/tradefed/testtype/GTestBase.java b/test_framework/com/android/tradefed/testtype/GTestBase.java
index 194199c..3ee7179 100644
--- a/test_framework/com/android/tradefed/testtype/GTestBase.java
+++ b/test_framework/com/android/tradefed/testtype/GTestBase.java
@@ -163,6 +163,11 @@
                             + "the same name as the binary with the .json extension.")
     private String mTestFilterKey = null;
 
+    @Option(
+            name = "disable-duplicate-test-check",
+            description = "If set to true, it will not check that a method is only run once.")
+    private boolean mDisableDuplicateCheck = false;
+
     // GTest flags...
     protected static final String GTEST_FLAG_PRINT_TIME = "--gtest_print_time";
     protected static final String GTEST_FLAG_FILTER = "--gtest_filter";
@@ -673,4 +678,17 @@
         }
         return new CoverageOptions();
     }
+
+    /**
+     * Returns the {@link GTestListener} that provides extra debugging info, like detects and
+     * reports duplicate tests if mDisabledDuplicateCheck is false. Otherwise, returns the passed-in
+     * listener.
+     */
+    protected ITestInvocationListener getGTestListener(ITestInvocationListener listener) {
+        if (mDisableDuplicateCheck) {
+            return listener;
+        }
+
+        return new GTestListener(listener);
+    }
 }
diff --git a/test_framework/com/android/tradefed/testtype/GTestListener.java b/test_framework/com/android/tradefed/testtype/GTestListener.java
new file mode 100644
index 0000000..c967d06
--- /dev/null
+++ b/test_framework/com/android/tradefed/testtype/GTestListener.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype;
+
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.FailureDescription;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.ResultForwarder;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Internal listener to Trade Federation for {@link GTest}. Detect and report if duplciated tests
+ * are run.
+ */
+final class GTestListener extends ResultForwarder {
+
+    private Set<TestDescription> mTests = new HashSet<>();
+    private Set<TestDescription> mDuplicateTests = new HashSet<>();
+
+    public GTestListener(ITestInvocationListener... listeners) {
+        super(listeners);
+    }
+
+    @Override
+    public void testStarted(TestDescription test, long startTime) {
+        super.testStarted(test, startTime);
+        if (!mTests.add(test)) {
+            mDuplicateTests.add(test);
+        }
+    }
+
+    @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));
+            error.setFailureStatus(FailureStatus.TEST_FAILURE);
+            super.testRunFailed(error);
+        }
+        super.testRunEnded(elapsedTime, runMetrics);
+    }
+}
diff --git a/test_framework/com/android/tradefed/testtype/HostGTest.java b/test_framework/com/android/tradefed/testtype/HostGTest.java
index c4958b7..43d599e 100644
--- a/test_framework/com/android/tradefed/testtype/HostGTest.java
+++ b/test_framework/com/android/tradefed/testtype/HostGTest.java
@@ -246,6 +246,7 @@
                     String.format("%s is not executable!", gTestFile.getAbsolutePath()));
         }
 
+        listener = getGTestListener(listener);
         // TODO: Need to support XML test output based on isEnableXmlOutput
         IShellOutputReceiver resultParser = createResultParser(gTestFile.getName(), listener);
         String flags = getAllGTestFlags(gTestFile.getName());
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index ad894a9..ed1ba81 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -257,6 +257,7 @@
 import com.android.tradefed.testtype.DeviceTestSuiteTest;
 import com.android.tradefed.testtype.FakeTestTest;
 import com.android.tradefed.testtype.GTestListTestParserTest;
+import com.android.tradefed.testtype.GTestListenerTest;
 import com.android.tradefed.testtype.GTestResultParserTest;
 import com.android.tradefed.testtype.GTestTest;
 import com.android.tradefed.testtype.GTestXmlResultParserTest;
@@ -728,6 +729,7 @@
     GoogleBenchmarkResultParserTest.class,
     GoogleBenchmarkTestTest.class,
     GTestListTestParserTest.class,
+    GTestListenerTest.class,
     GTestResultParserTest.class,
     GTestTest.class,
     GTestXmlResultParserTest.class,
diff --git a/tests/src/com/android/tradefed/device/cloud/GceManagerTest.java b/tests/src/com/android/tradefed/device/cloud/GceManagerTest.java
index 5cf6f64..f3bdced 100644
--- a/tests/src/com/android/tradefed/device/cloud/GceManagerTest.java
+++ b/tests/src/com/android/tradefed/device/cloud/GceManagerTest.java
@@ -756,6 +756,7 @@
                                 EasyMock.anyObject(),
                                 EasyMock.eq("root@127.0.0.1"),
                                 EasyMock.eq("./bin/adb"),
+                                EasyMock.eq("wait-for-device"),
                                 EasyMock.eq("shell"),
                                 EasyMock.eq("bugreportz")))
                 .andReturn(res);
diff --git a/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
index cca825c..24418a6 100644
--- a/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
@@ -113,7 +113,7 @@
         Mockito.verify(mPerfettoMetricCollector).runHostCommand(Mockito.anyLong(),
                 Mockito.any(), Mockito.any(), Mockito.any());
         Mockito.verify(mMockListener)
-                .testLog(Mockito.eq("trace"), Mockito.eq(LogDataType.PB), Mockito.any());
+                .testLog(Mockito.eq("trace"), Mockito.eq(LogDataType.PERFETTO), Mockito.any());
         assertTrue("Expected two metrics that includes success status",
                 currentMetrics.get("perfetto_trace_extractor_status").getMeasurements()
                         .getSingleString().equals("1"));
@@ -184,7 +184,7 @@
         Mockito.verify(mPerfettoMetricCollector).runHostCommand(Mockito.anyLong(),
                 Mockito.any(), Mockito.any(), Mockito.any());
         Mockito.verify(mMockListener)
-                .testLog(Mockito.eq("trace"), Mockito.eq(LogDataType.PB), Mockito.any());
+                .testLog(Mockito.eq("trace"), Mockito.eq(LogDataType.PERFETTO), Mockito.any());
         assertTrue("Expected two metrics that includes failure status",
                 currentMetrics.get("perfetto_trace_extractor_status").getMeasurements()
                         .getSingleString().equals("0"));
diff --git a/tests/src/com/android/tradefed/testtype/GTestListenerTest.java b/tests/src/com/android/tradefed/testtype/GTestListenerTest.java
new file mode 100644
index 0000000..392ee75
--- /dev/null
+++ b/tests/src/com/android/tradefed/testtype/GTestListenerTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.FailureDescription;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
+
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.HashMap;
+
+/** Unit tests for {@link GTestListener}. */
+@RunWith(JUnit4.class)
+public class GTestListenerTest {
+    private ITestInvocationListener mMockListener = null;
+
+    /** Helper to initialize the various EasyMocks we'll need. */
+    @Before
+    public void setUp() throws Exception {
+        mMockListener = EasyMock.createMock(ITestInvocationListener.class);
+    }
+
+    /** Helper that replays all mocks. */
+    private void replayMocks() {
+        EasyMock.replay(mMockListener);
+    }
+
+    /** Helper that verifies all mocks. */
+    private void verifyMocks() {
+        EasyMock.verify(mMockListener);
+    }
+
+    /** Verify test passes without duplicate tests */
+    @Test
+    public void testNoDuplicateTests() throws DeviceNotAvailableException {
+        String moduleName = "testNoDuplicateTest";
+        String testClass = "testClass";
+        String testName1 = "testName1";
+        TestDescription testId1 = new TestDescription(testClass, testName1);
+        String testName2 = "testName2";
+        TestDescription testId2 = new TestDescription(testClass, testName2);
+
+        mMockListener.testRunStarted(EasyMock.eq(moduleName), EasyMock.eq(2));
+        mMockListener.testStarted(EasyMock.eq(testId1), EasyMock.anyLong());
+        mMockListener.testEnded(
+                EasyMock.eq(testId1),
+                EasyMock.anyLong(),
+                (HashMap<String, Metric>) EasyMock.anyObject());
+        mMockListener.testStarted(EasyMock.eq(testId2), EasyMock.anyLong());
+        mMockListener.testEnded(
+                EasyMock.eq(testId2),
+                EasyMock.anyLong(),
+                (HashMap<String, Metric>) EasyMock.anyObject());
+        mMockListener.testRunEnded(
+                EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
+        replayMocks();
+
+        HashMap<String, Metric> emptyMap = new HashMap<>();
+        GTestListener listener = new GTestListener(mMockListener);
+        listener.testRunStarted(moduleName, 2);
+        listener.testStarted(testId1);
+        listener.testEnded(testId1, emptyMap);
+        listener.testStarted(testId2);
+        listener.testEnded(testId2, emptyMap);
+        listener.testRunEnded(0, emptyMap);
+        verifyMocks();
+    }
+
+    /** Verify test with duplicate tests fails if option enabled */
+    @Test
+    public void testDuplicateTestsFailsWithOptionEnabled() throws DeviceNotAvailableException {
+        String moduleName = "testWithDuplicateTests";
+        String testClass = "testClass";
+        String testName1 = "testName1";
+        String duplicateTestsMessage = "The following tests ran more than once: ";
+        TestDescription testId1 = new TestDescription(testClass, testName1);
+
+        mMockListener.testRunStarted(EasyMock.eq(moduleName), EasyMock.eq(2));
+        mMockListener.testStarted(EasyMock.eq(testId1), EasyMock.anyLong());
+        mMockListener.testEnded(
+                EasyMock.eq(testId1),
+                EasyMock.anyLong(),
+                (HashMap<String, Metric>) EasyMock.anyObject());
+        mMockListener.testStarted(EasyMock.eq(testId1), EasyMock.anyLong());
+        mMockListener.testEnded(
+                EasyMock.eq(testId1),
+                EasyMock.anyLong(),
+                (HashMap<String, Metric>) EasyMock.anyObject());
+        Capture<FailureDescription> capturedFailureDescription = new Capture<>();
+        mMockListener.testRunFailed(EasyMock.capture(capturedFailureDescription));
+        mMockListener.testRunEnded(
+                EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
+        replayMocks();
+
+        HashMap<String, Metric> emptyMap = new HashMap<>();
+        GTestListener listener = new GTestListener(mMockListener);
+        listener.testRunStarted(moduleName, 2);
+        listener.testStarted(testId1);
+        listener.testEnded(testId1, emptyMap);
+        listener.testStarted(testId1);
+        listener.testEnded(testId1, emptyMap);
+        listener.testRunEnded(0, emptyMap);
+        FailureDescription failureDescription = capturedFailureDescription.getValue();
+        assertTrue(failureDescription.getErrorMessage().contains(duplicateTestsMessage));
+        assertTrue(FailureStatus.TEST_FAILURE.equals(failureDescription.getFailureStatus()));
+        verifyMocks();
+    }
+}
diff --git a/tests/src/com/android/tradefed/util/RunUtilTest.java b/tests/src/com/android/tradefed/util/RunUtilTest.java
index e11ebff..3387f4c 100644
--- a/tests/src/com/android/tradefed/util/RunUtilTest.java
+++ b/tests/src/com/android/tradefed/util/RunUtilTest.java
@@ -91,8 +91,10 @@
                     // Test if the binary does not exists, startProcess throws directly in this case
                     doThrow(
                                     new RuntimeException(
-                                            "Cannot run program \"\": error=2,"
-                                                    + "No such file or directory"))
+                                            String.format(
+                                                    "Cannot run program \"%s\": error=2,"
+                                                            + "No such file or directory",
+                                                    command[0])))
                             .when(mMockRunnableResult)
                             .startProcess();
                 } else {
@@ -138,6 +140,7 @@
                 IRunUtil.IRunnableResult.class);
         EasyMock.expect(mockRunnable.getCommand()).andReturn(new ArrayList<>());
         EasyMock.expect(mockRunnable.run()).andThrow(new RuntimeException());
+        EasyMock.expect(mockRunnable.getResult()).andReturn(null);
         mockRunnable.cancel(); // cancel due to exception
         mockRunnable.cancel(); // always ensure execution is cancelled
         EasyMock.replay(mockRunnable);
@@ -172,7 +175,7 @@
         CommandResult result = spyUtil.runTimedCmd(VERY_LONG_TIMEOUT_MS, "blahggggwarggg");
         assertEquals(CommandStatus.EXCEPTION, result.getStatus());
         assertEquals("", result.getStdout());
-        assertEquals("", result.getStderr());
+        assertTrue(result.getStderr().contains("Cannot run program \"blahggggwarggg\""));
     }
 
     /**