Avoid calling prepareListener once per attempt

Prepare the listener preparation once for the retry session
to make it cleaner.

Test: unit tests
Bug: 112664318
Change-Id: I1d3fd6d9a7b19504011386d0eb681bd84deafb6c
diff --git a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
index 5d7d060..e52eebe 100644
--- a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
+++ b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
@@ -72,10 +72,10 @@
     private IInvocationContext mModuleInvocationContext;
     private IConfiguration mModuleConfiguration;
     private ModuleListener mMainGranularRunListener;
+    private RetryLogSaverResultForwarder mRetryAttemptForwarder;
     private List<ITestInvocationListener> mModuleLevelListeners;
     private ILogSaver mLogSaver;
     private String mModuleId;
-    private boolean mIsMetricCollectorInitialized;
     private int mMaxRunLimit;
 
     // Tracking of the metrics
@@ -97,7 +97,6 @@
         mTest = test;
         mMainGranularRunListener = new ModuleListener(mainListener);
         mFailureListener = failureListener;
-        mIsMetricCollectorInitialized = false;
         mModuleLevelListeners = moduleLevelListeners;
         mMaxRunLimit = maxRunLimit;
     }
@@ -173,7 +172,7 @@
      *     TestFailureListener}, and wrapped by RunMetricsCollector and Module MetricCollector (if
      *     not initialized).
      */
-    private ITestInvocationListener prepareRunListener(int attemptNumber) {
+    private ITestInvocationListener initializeListeners() {
         List<ITestInvocationListener> currentTestListener = new ArrayList<>();
         // Add all the module level listeners, including TestFailureListener
         if (mModuleLevelListeners != null) {
@@ -181,52 +180,40 @@
         }
         currentTestListener.add(mMainGranularRunListener);
 
-        ITestInvocationListener runListener =
-                new LogSaverResultForwarder(mLogSaver, currentTestListener) {
-                    // Propagate the attempt number to TestRunStarted.
-                    @Override
-                    public void testRunStarted(String runName, int testCount) {
-                        testRunStarted(runName, testCount, attemptNumber);
-                    }
-                };
+        mRetryAttemptForwarder = new RetryLogSaverResultForwarder(mLogSaver, currentTestListener);
+        ITestInvocationListener runListener = mRetryAttemptForwarder;
         if (mFailureListener != null) {
-            mFailureListener.setLogger(runListener);
+            mFailureListener.setLogger(mRetryAttemptForwarder);
             currentTestListener.add(mFailureListener);
         }
 
-        // TODO: For RunMetricCollector and moduleMetricCollector, we only gather the
-        // metrics in the first run. This part can be improved if we want to gather metrics for
-        // every run.
-        if (!mIsMetricCollectorInitialized) {
-            if (mRunMetricCollectors != null) {
-                // Module only init the collectors here to avoid triggering the collectors when
-                // replaying the cached events at the end. This ensure metrics are capture at
-                // the proper time in the invocation.
-                for (IMetricCollector collector : mRunMetricCollectors) {
-                    runListener = collector.init(mModuleInvocationContext, runListener);
-                }
-            }
-            // The module collectors itself are added: this list will be very limited.
-            for (IMetricCollector collector : mModuleConfiguration.getMetricCollectors()) {
+        if (mRunMetricCollectors != null) {
+            // Module only init the collectors here to avoid triggering the collectors when
+            // replaying the cached events at the end. This ensure metrics are capture at
+            // the proper time in the invocation.
+            for (IMetricCollector collector : mRunMetricCollectors) {
                 runListener = collector.init(mModuleInvocationContext, runListener);
             }
-            mIsMetricCollectorInitialized = true;
         }
+        // The module collectors itself are added: this list will be very limited.
+        for (IMetricCollector collector : mModuleConfiguration.getMetricCollectors()) {
+            runListener = collector.init(mModuleInvocationContext, runListener);
+        }
+
         return runListener;
     }
 
     /**
-     * Schedule a series of {@link IRemoteTest} "run". TODO: Customize the retry strategy; Each run
-     * is granularized to a subset of the whole testcases.
+     * Schedule a series of {@link IRemoteTest#run(ITestInvocationListener)}.
      *
      * @param listener The ResultForwarder listener which contains a new moduleListener for each
      *     run.
      */
     @Override
     public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
+        ITestInvocationListener allListeners = initializeListeners();
         // First do the regular run, not retried.
-        int initAttempt = 0;
-        intraModuleRun(initAttempt);
+        intraModuleRun(allListeners);
 
         if (mMaxRunLimit <= 1) {
             return;
@@ -234,10 +221,9 @@
 
         // If the very first attempt failed, then don't proceed.
         if (RetryStrategy.RERUN_UNTIL_FAILURE.equals(mRetryStrategy)) {
-            Set<TestDescription> lastRun = getFailedTestCases(initAttempt);
+            Set<TestDescription> lastRun = getFailedTestCases(0);
             // If we encountered a failure
-            if (!lastRun.isEmpty()
-                    || mMainGranularRunListener.hasRunCrashedAtAttempt(initAttempt)) {
+            if (!lastRun.isEmpty() || mMainGranularRunListener.hasRunCrashedAtAttempt(0)) {
                 CLog.w("%s failed after the first run. Stopping.", lastRun);
                 return;
             }
@@ -303,7 +289,7 @@
                 }
 
                 // Run the tests again
-                intraModuleRun(attemptNumber);
+                intraModuleRun(allListeners);
 
                 Set<TestDescription> lastRun = getFailedTestCases(attemptNumber);
                 if (shouldHandleFailure(mRetryStrategy)) {
@@ -378,13 +364,9 @@
         }
     }
 
-    /**
-     * The workflow for each individual {@link IRemoteTest} run. TODO: When this function is called,
-     * the IRemoteTest should already has the subset of testcases identified.
-     */
-    @VisibleForTesting
-    final void intraModuleRun(int attemptNumber) throws DeviceNotAvailableException {
-        ITestInvocationListener runListener = prepareRunListener(attemptNumber);
+    /** The workflow for each individual {@link IRemoteTest} run. */
+    private final void intraModuleRun(ITestInvocationListener runListener)
+            throws DeviceNotAvailableException {
         try {
             mTest.run(runListener);
         } catch (RuntimeException re) {
@@ -401,6 +383,8 @@
             CLog.w(due);
             CLog.w("Proceeding to the next test.");
             runListener.testRunFailed(due.getMessage());
+        } finally {
+            mRetryAttemptForwarder.incrementAttempt();
         }
     }
 
@@ -471,4 +455,25 @@
     public ModuleListener getResultListener() {
         return mMainGranularRunListener;
     }
+
+    /** Forwarder that also handles passing the current attempt we are at. */
+    private class RetryLogSaverResultForwarder extends LogSaverResultForwarder {
+
+        private int mAttemptNumber = 0;
+
+        public RetryLogSaverResultForwarder(
+                ILogSaver logSaver, List<ITestInvocationListener> listeners) {
+            super(logSaver, listeners);
+        }
+
+        @Override
+        public void testRunStarted(String runName, int testCount) {
+            testRunStarted(runName, testCount, mAttemptNumber);
+        }
+
+        /** Increment the attempt number. */
+        public void incrementAttempt() {
+            mAttemptNumber++;
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java b/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
index 6d8e39a..26c91e5 100644
--- a/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
@@ -18,6 +18,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.android.ddmlib.testrunner.TestResult.TestStatus;
 import com.android.tradefed.config.IConfiguration;
@@ -248,43 +249,25 @@
     }
 
     /**
-     * Test the intra module "run" triggers IRemoteTest run method with a dedicated ModuleListener.
+     * Test that the intra module retry does not inhibit DeviceNotAvailableException. They are
+     * bubbled up to the top.
      */
     @Test
-    public void testIntraModuleRun_pass() throws Exception {
-        TestDescription fakeTestCase = new TestDescription("ClassFoo", "TestFoo");
-
-        GranularRetriableTestWrapper granularTestWrapper =
-                createGranularTestWrapper(new FakeTest(), 99);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_CASE_FAILURE);
-        assertEquals(0, granularTestWrapper.getTestRunResultCollected().size());
-        granularTestWrapper.intraModuleRun(0);
-        assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
-        assertEquals(1, granularTestWrapper.getFinalTestRunResults().size());
-        Set<TestDescription> completedTests =
-                granularTestWrapper.getFinalTestRunResults().get(0).getCompletedTests();
-        assertEquals(1, completedTests.size());
-        assertTrue(completedTests.contains(fakeTestCase));
-
-        // No retried run because all tests passed
-        assertEquals(0, granularTestWrapper.getRetrySuccess());
-        assertEquals(0, granularTestWrapper.getRetryFailed());
-        assertEquals(0, granularTestWrapper.getRetryTime());
-    }
-
-    /**
-     * Test that the intra module "run" method catches DeviceNotAvailableException and raises it
-     * after record the tests.
-     */
-    @Test(expected = DeviceNotAvailableException.class)
     public void testIntraModuleRun_catchDeviceNotAvailableException() throws Exception {
         IRemoteTest mockTest = Mockito.mock(IRemoteTest.class);
         Mockito.doThrow(new DeviceNotAvailableException("fake message", "serial"))
                 .when(mockTest)
                 .run(Mockito.any(ITestInvocationListener.class));
+
         GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(mockTest, 1);
-        // Verify.
-        granularTestWrapper.intraModuleRun(0);
+        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_CASE_FAILURE);
+        try {
+            granularTestWrapper.run(new CollectingTestListener());
+            fail("Should have thrown an exception.");
+        } catch (DeviceNotAvailableException expected) {
+            // Expected
+            assertEquals("fake message", expected.getMessage());
+        }
     }
 
     /**
@@ -303,7 +286,7 @@
                     }
                 };
         GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(test, 1);
-        granularTestWrapper.intraModuleRun(0);
+        granularTestWrapper.run(new CollectingTestListener());
         TestRunResult attempResults =
                 granularTestWrapper.getTestRunResultCollected().get(RUN_NAME).get(0);
         assertTrue(attempResults.isRunFailure());