When a DNAE occurs report the non-executed tests

Allow test types to report their non-executed tests if all
the tests poller stops and some tests cannot be executed.

Test: unit tests
run cts --shard-count 2 then disconnect all devices
Bug: 111141078

Change-Id: If82bed881b6f774a53ca412ad2e6c4f1e55be787
Merged-In: If82bed881b6f774a53ca412ad2e6c4f1e55be787
diff --git a/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java b/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java
index e3a6cb5..2a50498 100644
--- a/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java
+++ b/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java
@@ -36,8 +36,10 @@
 import com.android.tradefed.testtype.IInvocationContextReceiver;
 import com.android.tradefed.testtype.IMultiDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.IReportNotExecuted;
 import com.android.tradefed.testtype.ITestCollector;
 import com.android.tradefed.util.StreamUtil;
+import com.android.tradefed.util.TimeUtil;
 
 import java.util.Collection;
 import java.util.HashMap;
@@ -162,6 +164,11 @@
             }
         } finally {
             mTracker.countDown();
+            if (mTracker.getCount() == 0) {
+                // If the last poller is also disconnected we want to know about the tests that
+                // did not execute.
+                reportNotExecuted(listener);
+            }
         }
     }
 
@@ -173,7 +180,9 @@
             throws DeviceNotAvailableException {
         try {
             if (mTracker.getCount() > 1) {
-                CLog.d("Wait 5 min for device to maybe coming back online.");
+                CLog.d(
+                        "Wait %s for device to maybe come back online.",
+                        TimeUtil.formatElapsedTime(WAIT_RECOVERY_TIME));
                 mDevice.waitForDeviceAvailable(WAIT_RECOVERY_TIME);
                 mDevice.reboot();
                 CLog.d("TestPoller was recovered after %s went offline", mDevice.getSerialNumber());
@@ -196,6 +205,21 @@
         throw originalException;
     }
 
+    /** Go through the remaining IRemoteTest and report them as not executed. */
+    private void reportNotExecuted(ITestInvocationListener listener) {
+        IRemoteTest test = poll();
+        while (test != null) {
+            if (test instanceof IReportNotExecuted) {
+                ((IReportNotExecuted) test).reportNotExecuted(listener);
+            } else {
+                CLog.e(
+                        "Could not report not executed tests from %s.",
+                        test.getClass().getCanonicalName());
+            }
+            test = poll();
+        }
+    }
+
     /** Helper to log the device events. */
     private void logDeviceEvent(EventType event, String serial, Throwable t) {
         Map<String, String> args = new HashMap<>();
diff --git a/src/com/android/tradefed/testtype/IReportNotExecuted.java b/src/com/android/tradefed/testtype/IReportNotExecuted.java
new file mode 100644
index 0000000..941f7f3
--- /dev/null
+++ b/src/com/android/tradefed/testtype/IReportNotExecuted.java
@@ -0,0 +1,35 @@
+/*
+ * 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.testtype;
+
+import com.android.tradefed.result.ITestInvocationListener;
+
+/**
+ * In case of an incomplete execution, {@link IRemoteTest} that implements this interface may report
+ * their non-executed tests for improved reporting.
+ */
+public interface IReportNotExecuted {
+
+    public static final String NOT_EXECUTED_FAILURE = "Test did not run. This is a placeholder.";
+
+    /**
+     * Report the non-executed tests to the main listener provided. They should be reported as
+     * failed with the {@link #NOT_EXECUTED_FAILURE} message.
+     *
+     * @param listener the main listener where to report the non-executed results.
+     */
+    public void reportNotExecuted(ITestInvocationListener listener);
+}
diff --git a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
index 4cdebe6..889d27b 100644
--- a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
+++ b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
@@ -270,7 +270,7 @@
      * the IRemoteTest should already has the subset of testcases identified.
      */
     @VisibleForTesting
-    final void intraModuleRun() throws DeviceNotAvailableException, DeviceUnresponsiveException {
+    final void intraModuleRun() throws DeviceNotAvailableException {
         ITestInvocationListener runListener = prepareRunListener();
         try {
             mTest.run(runListener);
@@ -288,8 +288,6 @@
             CLog.w(due);
             CLog.w("Proceeding to the next test.");
             runListener.testRunFailed(due.getMessage());
-        } catch (DeviceNotAvailableException dnae) {
-            throw dnae;
         } finally {
             ModuleListener currentModuleListener =
                     mModuleListenerCollector.get(mModuleListenerCollector.size() - 1);
diff --git a/src/com/android/tradefed/testtype/suite/ITestSuite.java b/src/com/android/tradefed/testtype/suite/ITestSuite.java
index c72ba11..f6c97a4 100644
--- a/src/com/android/tradefed/testtype/suite/ITestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/ITestSuite.java
@@ -50,6 +50,7 @@
 import com.android.tradefed.testtype.IInvocationContextReceiver;
 import com.android.tradefed.testtype.IMultiDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.IReportNotExecuted;
 import com.android.tradefed.testtype.IRuntimeHintProvider;
 import com.android.tradefed.testtype.IShardableTest;
 import com.android.tradefed.testtype.ITestCollector;
@@ -86,7 +87,8 @@
                 IInvocationContextReceiver,
                 IRuntimeHintProvider,
                 IMetricCollectorReceiver,
-                IConfigurationReceiver {
+                IConfigurationReceiver,
+                IReportNotExecuted {
 
     public static final String SKIP_SYSTEM_STATUS_CHECKER = "skip-system-status-check";
     public static final String RUNNER_WHITELIST = "runner-whitelist";
@@ -792,6 +794,21 @@
         mMainConfiguration = configuration;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void reportNotExecuted(ITestInvocationListener listener) {
+        List<ModuleDefinition> runModules = createExecutionList();
+
+        while (!runModules.isEmpty()) {
+            ModuleDefinition module = runModules.remove(0);
+            listener.testModuleStarted(module.getModuleInvocationContext());
+            listener.testRunStarted(module.getId(), 0);
+            listener.testRunFailed(IReportNotExecuted.NOT_EXECUTED_FAILURE);
+            listener.testRunEnded(0, new HashMap<String, Metric>());
+            listener.testModuleEnded();
+        }
+    }
+
     /**
      * Returns the {@link ModuleDefinition} to be executed directly, or null if none yet (when the
      * ITestSuite has not been sharded yet).
diff --git a/tests/src/com/android/tradefed/invoker/shard/TestsPoolPollerTest.java b/tests/src/com/android/tradefed/invoker/shard/TestsPoolPollerTest.java
index 53cec94..8686306 100644
--- a/tests/src/com/android/tradefed/invoker/shard/TestsPoolPollerTest.java
+++ b/tests/src/com/android/tradefed/invoker/shard/TestsPoolPollerTest.java
@@ -29,7 +29,10 @@
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.IReportNotExecuted;
 import com.android.tradefed.testtype.StubTest;
+import com.android.tradefed.testtype.suite.ITestSuite;
+import com.android.tradefed.testtype.suite.ITestSuiteTest.TestSuiteImpl;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -213,6 +216,56 @@
     }
 
     /**
+     * Test that a runner that implements {@link IReportNotExecuted} can report the non-executed
+     * tests when the DNAE occurs.
+     */
+    @Test
+    public void testRun_dnae_reportNotExecuted() throws Exception {
+        List<IRemoteTest> testsList = new ArrayList<>();
+        // Add one bad test first that will throw an exception.
+        IRemoteTest badTest = new StubTest();
+        OptionSetter setter = new OptionSetter(badTest);
+        setter.setOptionValue("test-throw-not-available", "true");
+        testsList.add(badTest);
+        // Add tests that from a suite that can report their not executed tests.
+        int numTests = 5;
+        ITestSuite suite = new TestSuiteImpl(numTests);
+        testsList.addAll(suite.split(3));
+        CountDownLatch tracker = new CountDownLatch(1);
+        TestsPoolPoller poller = new TestsPoolPoller(testsList, tracker);
+        poller.setMetricCollectors(mMetricCollectors);
+        poller.setDevice(mDevice);
+        poller.setLogRegistry(mMockRegistry);
+        try {
+            poller.run(mListener);
+            fail("Should have thrown an exception.");
+        } catch (DeviceNotAvailableException expected) {
+            // expected
+        }
+        // We expect all the non-executed tests to be reported.
+        Mockito.verify(mListener, Mockito.times(1))
+                .testRunStarted(Mockito.eq("test"), Mockito.eq(0));
+        Mockito.verify(mListener, Mockito.times(1))
+                .testRunStarted(Mockito.eq("test1"), Mockito.eq(0));
+        Mockito.verify(mListener, Mockito.times(1))
+                .testRunStarted(Mockito.eq("test2"), Mockito.eq(0));
+        Mockito.verify(mListener, Mockito.times(1))
+                .testRunStarted(Mockito.eq("test3"), Mockito.eq(0));
+        Mockito.verify(mListener, Mockito.times(1))
+                .testRunStarted(Mockito.eq("test4"), Mockito.eq(0));
+        Mockito.verify(mListener, Mockito.times(5))
+                .testRunFailed(IReportNotExecuted.NOT_EXECUTED_FAILURE);
+        Mockito.verify(mListener, Mockito.times(5))
+                .testRunEnded(Mockito.anyLong(), (HashMap<String, Metric>) Mockito.any());
+        assertEquals(0, tracker.getCount());
+        Mockito.verify(mMockRegistry)
+                .logEvent(
+                        Mockito.eq(LogLevel.DEBUG),
+                        Mockito.eq(EventType.SHARD_POLLER_EARLY_TERMINATION),
+                        Mockito.any());
+    }
+
+    /**
      * If a device not available exception is thrown from a tests, and the poller is not the last
      * one alive, we wait and attempt to recover the device. In case of success, execution will
      * proceed.
diff --git a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
index cd73b60..25af8af 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
@@ -93,10 +93,13 @@
     private IConfiguration mStubMainConfiguration;
     private ILogSaver mMockLogSaver;
 
-    /**
-     * Very basic implementation of {@link ITestSuite} to test it.
-     */
-    static class TestSuiteImpl extends ITestSuite {
+    // Guice scope and objects for testing
+    private InvocationScope mScope;
+    private Injector mInjector;
+    private InvocationScopeModule mInvocationScope;
+
+    /** Very basic implementation of {@link ITestSuite} to test it. */
+    public static class TestSuiteImpl extends ITestSuite {
         private int mNumTests = 1;
 
         public TestSuiteImpl() {}