Call tearDown before reportLogs + invocationEnded.

Bug 8300094

Change-Id: Idd671820f77468b45e97518d4d3a6a84352a18be
diff --git a/src/com/android/tradefed/invoker/ITestInvocation.java b/src/com/android/tradefed/invoker/ITestInvocation.java
index 210f6bc..68c3098 100644
--- a/src/com/android/tradefed/invoker/ITestInvocation.java
+++ b/src/com/android/tradefed/invoker/ITestInvocation.java
@@ -34,8 +34,9 @@
      * @param rescheduler the {@link IRescheduler}, for rescheduling portions of the invocation for
      *            execution on another resource(s)
      * @throws DeviceNotAvailableException if communication with device was lost
+     * @throws Throwable
      */
     public void invoke(ITestDevice device, IConfiguration config, IRescheduler rescheduler)
-            throws DeviceNotAvailableException;
+            throws DeviceNotAvailableException, Throwable;
 
 }
diff --git a/src/com/android/tradefed/invoker/TestInvocation.java b/src/com/android/tradefed/invoker/TestInvocation.java
index 7f1ddfd..cbed078 100644
--- a/src/com/android/tradefed/invoker/TestInvocation.java
+++ b/src/com/android/tradefed/invoker/TestInvocation.java
@@ -103,7 +103,7 @@
      */
     @Override
     public void invoke(ITestDevice device, IConfiguration config, IRescheduler rescheduler)
-            throws DeviceNotAvailableException {
+            throws DeviceNotAvailableException, Throwable {
         try {
             mStatus = "fetching build";
             config.getLogOutput().init();
@@ -266,37 +266,29 @@
      * @param config the {@link IConfiguration}
      * @param device the {@link ITestDevice} to use. May be <code>null</code>
      * @param info the {@link IBuildInfo}
-     *
-     * @throws DeviceNotAvailableException
-     * @throws IOException if log could not be created
      */
     private void performInvocation(IConfiguration config, ITestDevice device, IBuildInfo info,
-            IRescheduler rescheduler) throws DeviceNotAvailableException, IOException {
+            IRescheduler rescheduler) throws Throwable {
 
         boolean resumed = false;
         long startTime = System.currentTimeMillis();
         long elapsedTime = -1;
-        Throwable exception = null;
 
         info.setDeviceSerial(device.getSerialNumber());
         startInvocation(config, device, info);
         try {
             device.setOptions(config.getDeviceOptions());
-            for (ITargetPreparer preparer : config.getTargetPreparers()) {
-                preparer.setUp(device, info);
-            }
-            runTests(device, info, config, rescheduler);
+
+            prepareAndRun(config, device, info, rescheduler);
         } catch (BuildError e) {
             CLog.w("Build %s failed on device %s. Reason: %s", info.getBuildId(),
                     device.getSerialNumber(), e.toString());
             takeBugreport(device, config.getTestInvocationListeners());
             reportFailure(e, config.getTestInvocationListeners(), config, info, rescheduler);
-            exception = e;
         } catch (TargetSetupError e) {
             CLog.e("Caught exception while running invocation");
             CLog.e(e);
             reportFailure(e, config.getTestInvocationListeners(), config, info, rescheduler);
-            exception = e;
         } catch (DeviceNotAvailableException e) {
             // log a warning here so its captured before reportLogs is called
             CLog.w("Invocation did not complete due to device %s becoming not available. " +
@@ -307,18 +299,15 @@
             } else {
                 CLog.i("Rescheduled failed invocation for resume");
             }
-            exception = e;
             throw e;
         } catch (RuntimeException e) {
             // log a warning here so its captured before reportLogs is called
             CLog.w("Unexpected exception when running invocation: %s", e.toString());
             reportFailure(e, config.getTestInvocationListeners(), config, info, rescheduler);
-            exception = e;
             throw e;
         } catch (AssertionError e) {
             CLog.w("Caught AssertionError while running invocation: ", e.toString());
             reportFailure(e, config.getTestInvocationListeners(), config, info, rescheduler);
-            exception = e;
         } finally {
             mStatus = "done running tests";
             try {
@@ -329,16 +318,6 @@
                             config.getTestInvocationListeners(), elapsedTime);
                 }
 
-                for (ITargetPreparer preparer : config.getTargetPreparers()) {
-                    // Note: adjusted indentation below for legibility.  If preparer is an
-                    // ITargetCleaner and we didn't hit DeviceNotAvailableException, then...
-                    if (preparer instanceof ITargetCleaner &&
-                            !(exception != null &&
-                              exception instanceof DeviceNotAvailableException)) {
-                        ITargetCleaner cleaner = (ITargetCleaner) preparer;
-                        cleaner.tearDown(device, info, exception);
-                    }
-                }
             } finally {
                 config.getBuildProvider().cleanUp(info);
             }
@@ -346,6 +325,54 @@
     }
 
     /**
+     * Do setup, run the tests, then call tearDown
+     */
+    private void prepareAndRun(IConfiguration config, ITestDevice device, IBuildInfo info,
+            IRescheduler rescheduler) throws Throwable {
+        // use the JUnit3 logic for handling exceptions when running tests
+        Throwable exception = null;
+
+        try {
+            doSetup(config, device, info);
+            runTests(device, info, config, rescheduler);
+        } catch (Throwable running) {
+            exception = running;
+        } finally {
+            try {
+                doTeardown(config, device, info, exception);
+            } catch (Throwable tearingDown) {
+                if (exception == null) {
+                    exception = tearingDown;
+                }
+            }
+        }
+        if (exception != null) {
+            throw exception;
+        }
+    }
+
+    private void doSetup(IConfiguration config, ITestDevice device, IBuildInfo info)
+            throws TargetSetupError, BuildError, DeviceNotAvailableException {
+        for (ITargetPreparer preparer : config.getTargetPreparers()) {
+            preparer.setUp(device, info);
+        }
+    }
+
+    private void doTeardown(IConfiguration config, ITestDevice device, IBuildInfo info,
+            Throwable exception) throws DeviceNotAvailableException {
+        for (ITargetPreparer preparer : config.getTargetPreparers()) {
+            // Note: adjusted indentation below for legibility.  If preparer is an
+            // ITargetCleaner and we didn't hit DeviceNotAvailableException, then...
+            if (preparer instanceof ITargetCleaner &&
+                    !(exception != null &&
+                      exception instanceof DeviceNotAvailableException)) {
+                ITargetCleaner cleaner = (ITargetCleaner) preparer;
+                cleaner.tearDown(device, info, exception);
+            }
+        }
+    }
+
+    /**
      * Starts the invocation.
      * <p/>
      * Starts logging, and informs listeners that invocation has been started.
diff --git a/tests/src/com/android/tradefed/command/CommandSchedulerTest.java b/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
index 4a47d19..d58f722 100644
--- a/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
+++ b/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
@@ -146,7 +146,7 @@
     /**
      * Test {@link CommandScheduler#run()} when one config has been added
      */
-    public void testRun_oneConfig() throws Exception {
+    public void testRun_oneConfig() throws Throwable {
         String[] args = new String[] {};
         mMockManager.setNumDevices(2);
         setCreateConfigExpectations(args, 1);
@@ -163,7 +163,7 @@
     /**
      * Test {@link CommandScheduler#run()} when one config has been added in dry-run mode
      */
-    public void testRun_dryRun() throws Exception {
+    public void testRun_dryRun() throws Throwable {
         String[] dryRunArgs = new String[] {"--dry-run"};
         mCommandOptions.setDryRunMode(true);
         mMockManager.setNumDevices(2);
@@ -192,7 +192,7 @@
      *
      * @param times
      */
-    private void setExpectedInvokeCalls(int times) throws DeviceNotAvailableException {
+    private void setExpectedInvokeCalls(int times) throws Throwable {
         mMockInvocation.invoke((ITestDevice)EasyMock.anyObject(),
                 (IConfiguration)EasyMock.anyObject(), (IRescheduler)EasyMock.anyObject());
         EasyMock.expectLastCall().times(times);
@@ -204,7 +204,7 @@
      *
      * @param times
      */
-    private Object waitForExpectedInvokeCalls(final int times) throws DeviceNotAvailableException {
+    private Object waitForExpectedInvokeCalls(final int times) throws Throwable {
         IAnswer<Object> blockResult = new IAnswer<Object>() {
             private int mCalls = 0;
             @Override
@@ -228,7 +228,7 @@
     /**
      * Test {@link CommandScheduler#run()} when one config has been added in a loop
      */
-    public void testRun_oneConfigLoop() throws Exception {
+    public void testRun_oneConfigLoop() throws Throwable {
         String[] args = new String[] {};
         // track if exception occurs on scheduler thread
         UncaughtExceptionHandler defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
@@ -274,7 +274,7 @@
     /**
      * Verify that scheduler goes into shutdown mode when a {@link FatalHostError} is thrown.
      */
-    public void testRun_fatalError() throws Exception {
+    public void testRun_fatalError() throws Throwable {
         mMockInvocation.invoke((ITestDevice)EasyMock.anyObject(),
                 (IConfiguration)EasyMock.anyObject(), (IRescheduler)EasyMock.anyObject());
         EasyMock.expectLastCall().andThrow(new FatalHostError("error"));
@@ -295,7 +295,7 @@
      * <p/>
      * Adds two configs to run, and verify they both run on one device
      */
-    public void testRun_configSerial() throws Exception {
+    public void testRun_configSerial() throws Throwable {
         String[] args = new String[] {};
         mMockManager.setNumDevices(2);
         setCreateConfigExpectations(args, 2);
@@ -322,7 +322,7 @@
      * <p/>
      * Adds two configs to run, and verify they both run on the other device
      */
-    public void testRun_configExcludeSerial() throws Exception {
+    public void testRun_configExcludeSerial() throws Throwable {
         String[] args = new String[] {};
         mMockManager.setNumDevices(2);
         setCreateConfigExpectations(args, 2);
@@ -348,7 +348,7 @@
      * Test {@link CommandScheduler#run()} when one config has been rescheduled
      */
     @SuppressWarnings("unchecked")
-    public void testRun_rescheduled() throws Exception {
+    public void testRun_rescheduled() throws Throwable {
         String[] args = new String[] {};
         mMockManager.setNumDevices(2);
         setCreateConfigExpectations(args, 1);
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
index aebcaec..d47d426 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
@@ -21,7 +21,6 @@
 import com.android.tradefed.build.IBuildProvider;
 import com.android.tradefed.command.FatalHostError;
 import com.android.tradefed.config.Configuration;
-import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.IDeviceRecovery;
@@ -37,12 +36,14 @@
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.TestSummary;
 import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.ITargetCleaner;
 import com.android.tradefed.targetprep.ITargetPreparer;
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.IResumableTest;
 import com.android.tradefed.testtype.IRetriableTest;
 
+import junit.framework.AssertionFailedError;
 import junit.framework.Test;
 import junit.framework.TestCase;
 
@@ -141,7 +142,7 @@
      * <p/>
      * Verifies that all external interfaces get notified as expected.
      */
-    public void testInvoke_RemoteTest() throws Exception {
+    public void testInvoke_RemoteTest() throws Throwable {
         IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
         setupMockSuccessListeners();
 
@@ -160,7 +161,7 @@
      * TODO: For results_reporters to work as both ITestInvocationListener and ITestSummaryListener,
      * TODO: this test _must_ pass.  Currently, it does not, so that mode of usage is not supported.
      */
-    public void DISABLED_testInvoke_twoSummary() throws Exception {
+    public void DISABLED_testInvoke_twoSummary() throws Throwable {
 
         IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
         setupMockSuccessListeners();
@@ -177,8 +178,7 @@
      * <p/>
      * An invocation will be started in this scenario.
      */
-    public void testInvoke_buildFailed() throws BuildRetrievalError, ConfigurationException,
-            DeviceNotAvailableException  {
+    public void testInvoke_buildFailed() throws Throwable  {
         BuildRetrievalError exception = new BuildRetrievalError("error", null, mMockBuildInfo);
         EasyMock.expect(mMockBuildProvider.getBuild()).andThrow(exception);
         setupMockFailureListeners(exception);
@@ -196,8 +196,7 @@
     /**
      * Test the invoke scenario where there is no build to test.
      */
-    public void testInvoke_noBuild() throws BuildRetrievalError, ConfigurationException,
-            DeviceNotAvailableException  {
+    public void testInvoke_noBuild() throws Throwable  {
         EasyMock.expect(mMockBuildProvider.getBuild()).andReturn(null);
         IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
         mStubConfiguration.setTest(test);
@@ -210,8 +209,7 @@
     /**
      * Test the invoke scenario where there is no build to test for a {@link IRetriableTest}.
      */
-    public void testInvoke_noBuildRetry() throws BuildRetrievalError, ConfigurationException,
-            DeviceNotAvailableException  {
+    public void testInvoke_noBuildRetry() throws Throwable  {
         EasyMock.expect(mMockBuildProvider.getBuild()).andReturn(null);
 
         IRetriableTest test = EasyMock.createMock(IRetriableTest.class);
@@ -234,7 +232,7 @@
      * Test the {@link TestInvocation#invoke(IDevice, IConfiguration)} scenario where the
      * test is a {@link IDeviceTest}
      */
-    public void testInvoke_deviceTest() throws Exception {
+    public void testInvoke_deviceTest() throws Throwable {
          DeviceConfigTest mockDeviceTest = EasyMock.createMock(DeviceConfigTest.class);
          mStubConfiguration.setTest(mockDeviceTest);
          mockDeviceTest.setDevice(mMockDevice);
@@ -251,7 +249,7 @@
      *
      * @throws Exception if unexpected error occurs
      */
-    public void testInvoke_testFail() throws Exception {
+    public void testInvoke_testFail() throws Throwable {
         IllegalArgumentException exception = new IllegalArgumentException();
         IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
         test.run((ITestInvocationListener)EasyMock.anyObject());
@@ -274,7 +272,7 @@
      *
      * @throws Exception if unexpected error occurs
      */
-    public void testInvoke_fatalError() throws Exception {
+    public void testInvoke_fatalError() throws Throwable {
         FatalHostError exception = new FatalHostError("error");
         IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
         test.run((ITestInvocationListener)EasyMock.anyObject());
@@ -297,7 +295,7 @@
      *
      * @throws Exception if unexpected error occurs
      */
-    public void testInvoke_deviceNotAvail() throws Exception {
+    public void testInvoke_deviceNotAvail() throws Throwable {
         DeviceNotAvailableException exception = new DeviceNotAvailableException();
         IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
         test.run((ITestInvocationListener)EasyMock.anyObject());
@@ -320,7 +318,7 @@
      *
      * @throws Exception if unexpected error occurs
      */
-    public void testInvoke_buildError() throws Exception {
+    public void testInvoke_buildError() throws Throwable {
         BuildError exception = new BuildError("error");
         IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
         mStubConfiguration.setTest(test);
@@ -351,7 +349,7 @@
      *
      * @throws Exception if unexpected error occurs
      */
-    public void testInvoke_resume() throws Exception {
+    public void testInvoke_resume() throws Throwable {
         IResumableTest resumableTest = EasyMock.createMock(IResumableTest.class);
         mStubConfiguration.setTest(resumableTest);
         ITestInvocationListener resumeListener = EasyMock.createStrictMock(
@@ -427,7 +425,7 @@
      *
      * @throws Exception if unexpected error occurs
      */
-    public void testInvoke_retry() throws Exception {
+    public void testInvoke_retry() throws Throwable {
         AssertionError exception = new AssertionError();
         IRetriableTest test = EasyMock.createMock(IRetriableTest.class);
         test.run((ITestInvocationListener)EasyMock.anyObject());
@@ -448,11 +446,87 @@
     }
 
     /**
+     * Test the {@link TestInvocation#invoke(IDevice, IConfiguration)} scenario when a
+     * {@link ITargetCleaner} is part of the config
+     */
+    public void testInvoke_tearDown() throws Throwable {
+         IRemoteTest test = EasyMock.createNiceMock(IRemoteTest.class);
+         ITargetCleaner mockCleaner = EasyMock.createMock(ITargetCleaner.class);
+         mockCleaner.setUp(mMockDevice, mMockBuildInfo);
+         mockCleaner.tearDown(mMockDevice, mMockBuildInfo, null);
+         mStubConfiguration.getTargetPreparers().add(mockCleaner);
+         setupMockSuccessListeners();
+         setupNormalInvoke(test);
+         EasyMock.replay(mockCleaner);
+         mTestInvocation.invoke(mMockDevice, mStubConfiguration, new StubRescheduler());
+         verifyMocks(mockCleaner);
+         verifySummaryListener();
+    }
+
+    /**
+     * Test the {@link TestInvocation#invoke(IDevice, IConfiguration)} scenario when a
+     * {@link ITargetCleaner} is part of the config, and the test throws a
+     * {@link DeviceNotAvailableException}.
+     */
+    public void testInvoke_tearDown_deviceNotAvail() throws Throwable {
+        DeviceNotAvailableException exception = new DeviceNotAvailableException();
+        IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
+        test.run((ITestInvocationListener)EasyMock.anyObject());
+        EasyMock.expectLastCall().andThrow(exception);
+        ITargetCleaner mockCleaner = EasyMock.createMock(ITargetCleaner.class);
+        mockCleaner.setUp(mMockDevice, mMockBuildInfo);
+        // tearDown should NOT be called
+        // mockCleaner.tearDown(mMockDevice, mMockBuildInfo, null);
+        mStubConfiguration.getTargetPreparers().add(mockCleaner);
+        setupMockFailureListeners(exception);
+        mMockBuildProvider.buildNotTested(mMockBuildInfo);
+        setupNormalInvoke(test);
+        EasyMock.replay(mockCleaner);
+        try {
+            mTestInvocation.invoke(mMockDevice, mStubConfiguration, new StubRescheduler());
+            fail("DeviceNotAvailableException not thrown");
+        } catch (DeviceNotAvailableException e) {
+            // expected
+        }
+        verifyMocks(mockCleaner);
+        verifySummaryListener();
+    }
+
+    /**
+     * Test the {@link TestInvocation#invoke(IDevice, IConfiguration)} scenario when a
+     * {@link ITargetCleaner} is part of the config, and the test throws a
+     * {@link RuntimeException}.
+     */
+    public void testInvoke_tearDown_runtime() throws Throwable {
+        RuntimeException exception = new RuntimeException();
+        IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
+        test.run((ITestInvocationListener)EasyMock.anyObject());
+        EasyMock.expectLastCall().andThrow(exception);
+        ITargetCleaner mockCleaner = EasyMock.createMock(ITargetCleaner.class);
+        mockCleaner.setUp(mMockDevice, mMockBuildInfo);
+        // tearDown should be called
+        mockCleaner.tearDown(mMockDevice, mMockBuildInfo, exception);
+        mStubConfiguration.getTargetPreparers().add(mockCleaner);
+        setupMockFailureListeners(exception);
+        mMockBuildProvider.buildNotTested(mMockBuildInfo);
+        setupNormalInvoke(test);
+        EasyMock.replay(mockCleaner);
+        try {
+            mTestInvocation.invoke(mMockDevice, mStubConfiguration, new StubRescheduler());
+            fail("RuntimeException not thrown");
+        } catch (RuntimeException e) {
+            // expected
+        }
+        verifyMocks(mockCleaner);
+        verifySummaryListener();
+    }
+
+    /**
      * Set up expected conditions for normal run up to the part where tests are run.
      *
      * @param test the {@link Test} to use.
      */
-    private void setupNormalInvoke(IRemoteTest test) throws Exception {
+    private void setupNormalInvoke(IRemoteTest test) throws Throwable {
         mStubConfiguration.setTest(test);
         EasyMock.expect(mMockBuildProvider.getBuild()).andReturn(mMockBuildInfo);
         mMockDevice.setOptions((TestDeviceOptions)EasyMock.anyObject());
@@ -504,15 +578,16 @@
             mMockSummaryListener.invocationFailed(EasyMock.eq(throwable));
         }
 
+
         // testLog (mMockTestListener)
         mMockTestListener.testLog(EasyMock.eq(TestInvocation.DEVICE_LOG_NAME),
                 EasyMock.eq(LogDataType.TEXT), (InputStreamSource)EasyMock.anyObject());
+        // testLog (mMockSummaryListener)
+        mMockSummaryListener.testLog(EasyMock.eq(TestInvocation.DEVICE_LOG_NAME),
+                EasyMock.eq(LogDataType.TEXT), (InputStreamSource)EasyMock.anyObject());
         mMockTestListener.testLog(EasyMock.eq(TestInvocation.TRADEFED_LOG_NAME),
                 EasyMock.eq(LogDataType.TEXT), (InputStreamSource)EasyMock.anyObject());
 
-        // testLog (mMockSummaryListener)
-        mMockSummaryListener.testLog(EasyMock.eq(TestInvocation.DEVICE_LOG_NAME),
-                EasyMock.eq(LogDataType.TEXT), (InputStreamSource)EasyMock.anyObject());
         mMockSummaryListener.testLog(EasyMock.eq(TestInvocation.TRADEFED_LOG_NAME),
                 EasyMock.eq(LogDataType.TEXT), (InputStreamSource)EasyMock.anyObject());
 
@@ -543,20 +618,26 @@
     /**
      * Verify all mock objects received expected calls
      */
-    private void verifyMocks(IRemoteTest mockTest) {
+    private void verifyMocks(Object... mocks) {
         // note: intentionally exclude configuration from verification - don't care
         // what methods are called
-        EasyMock.verify(mockTest, mMockTestListener, mMockSummaryListener, mMockPreparer,
+        EasyMock.verify(mMockTestListener, mMockSummaryListener, mMockPreparer,
                 mMockBuildProvider, mMockBuildInfo, mMockLogRegistry, mMockLogger);
+        if (mocks.length > 0) {
+            EasyMock.verify(mocks);
+        }
     }
 
     /**
      * Switch all mock objects into replay mode.
      */
-    private void replayMocks(IRemoteTest mockTest) {
-        EasyMock.replay(mockTest, mMockTestListener, mMockSummaryListener,
+    private void replayMocks(Object... mocks) {
+        EasyMock.replay(mMockTestListener, mMockSummaryListener,
                 mMockPreparer, mMockBuildProvider, mMockLogger, mMockDevice, mMockBuildInfo,
                 mMockLogRegistry);
+        if (mocks.length > 0) {
+            EasyMock.replay(mocks);
+        }
     }
 
     /**