Start forwarding the new invoc failure event

Test: unit tests
Bug: 153374987
Change-Id: Iec1e7e9cd85462c40b9ad08d674bcafe0e288f6e
diff --git a/invocation_interfaces/com/android/tradefed/result/ITestInvocationListener.java b/invocation_interfaces/com/android/tradefed/result/ITestInvocationListener.java
index ed8e1b7..462f375 100644
--- a/invocation_interfaces/com/android/tradefed/result/ITestInvocationListener.java
+++ b/invocation_interfaces/com/android/tradefed/result/ITestInvocationListener.java
@@ -79,7 +79,7 @@
      *
      * @param failure the {@link FailureDescription} describing the cause of the failure
      */
-    public default void invocationFailed(FailureDescription failure) {
+    default void invocationFailed(FailureDescription failure) {
         if (failure.getCause() != null) {
             invocationFailed(failure.getCause());
         } else {
diff --git a/src/com/android/tradefed/invoker/ShardListener.java b/src/com/android/tradefed/invoker/ShardListener.java
index 2f06de0..71ad448 100644
--- a/src/com/android/tradefed/invoker/ShardListener.java
+++ b/src/com/android/tradefed/invoker/ShardListener.java
@@ -95,6 +95,15 @@
         }
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void invocationFailed(FailureDescription failure) {
+        super.invocationFailed(failure);
+        synchronized (mMasterListener) {
+            mMasterListener.invocationFailed(failure);
+        }
+    }
+
     /**
      * {@inheritDoc}
      */
diff --git a/src/com/android/tradefed/invoker/ShardMasterResultForwarder.java b/src/com/android/tradefed/invoker/ShardMasterResultForwarder.java
index 3be9f37..1e4b985 100644
--- a/src/com/android/tradefed/invoker/ShardMasterResultForwarder.java
+++ b/src/com/android/tradefed/invoker/ShardMasterResultForwarder.java
@@ -17,6 +17,7 @@
 
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.FailureDescription;
 import com.android.tradefed.result.ILogSaverListener;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.InputStreamSource;
@@ -90,6 +91,13 @@
         super.invocationFailed(cause);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void invocationFailed(FailureDescription failure) {
+        // one of the shards failed. Fail the whole invocation
+        super.invocationFailed(failure);
+    }
+
     /**
      * {@inheritDoc}
      */
diff --git a/src/com/android/tradefed/result/CollectingTestListener.java b/src/com/android/tradefed/result/CollectingTestListener.java
index d9ec6ec..230805e 100644
--- a/src/com/android/tradefed/result/CollectingTestListener.java
+++ b/src/com/android/tradefed/result/CollectingTestListener.java
@@ -152,6 +152,12 @@
         // ignore
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void invocationFailed(FailureDescription failure) {
+        // ignore
+    }
+
     @Override
     public void testModuleStarted(IInvocationContext moduleContext) {
         mCurrentModuleContext = moduleContext;
diff --git a/src/com/android/tradefed/result/proto/ProtoResultReporter.java b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
index 4e77622..d070fcc 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
@@ -67,6 +67,7 @@
     private IInvocationContext mContext;
 
     private Throwable mInvocationFailure = null;
+    private FailureDescription mInvocationFailureDescription = null;
     /** Whether or not a testModuleStart had currently been called. */
     private boolean mModuleInProgress = false;
 
@@ -177,6 +178,11 @@
     }
 
     @Override
+    public void invocationFailed(FailureDescription failure) {
+        mInvocationFailureDescription = failure;
+    }
+
+    @Override
     public final void invocationEnded(long elapsedTime) {
         if (mModuleInProgress) {
             // If we had a module in progress, and a new module start occurs, complete the call
@@ -188,21 +194,9 @@
         // Update the context in case it changed
         mInvocationRecordBuilder.setDescription(Any.pack(mContext.toProto()));
 
-        if (mInvocationFailure != null) {
-            DebugInfo.Builder debugBuilder = DebugInfo.newBuilder();
-            if (mInvocationFailure.getMessage() != null) {
-                debugBuilder.setErrorMessage(mInvocationFailure.getMessage());
-            }
-            debugBuilder.setTrace(StreamUtil.getStackTrace(mInvocationFailure));
-            DebugInfoContext.Builder debugContext = DebugInfoContext.newBuilder();
-            try {
-                debugContext.setErrorType(SerializationUtil.serializeToString(mInvocationFailure));
-            } catch (IOException e) {
-                CLog.e("Failed to serialize the invocation failure:");
-                CLog.e(e);
-            }
-            debugBuilder.setDebugInfoContext(debugContext);
-            mInvocationRecordBuilder.setDebugInfo(debugBuilder);
+        DebugInfo invocationFailure = handleInvocationFailure();
+        if (invocationFailure != null) {
+            mInvocationRecordBuilder.setDebugInfo(invocationFailure);
         }
 
         // Finalize the protobuf handling: where to put the results.
@@ -516,4 +510,39 @@
         }
         return logFileBuilder.build();
     }
+
+    private DebugInfo handleInvocationFailure() {
+        DebugInfo.Builder debugBuilder = DebugInfo.newBuilder();
+        Throwable baseException = null;
+        if (mInvocationFailureDescription != null) {
+            baseException = mInvocationFailureDescription.getCause();
+        }
+        if (baseException == null) {
+            baseException = mInvocationFailure;
+        }
+        // TODO: Handle FailureDescription without getCause.
+        if (baseException == null) {
+            // No invocation failure
+            return null;
+        }
+
+        if (baseException.getMessage() != null) {
+            debugBuilder.setErrorMessage(baseException.getMessage());
+        }
+        debugBuilder.setTrace(StreamUtil.getStackTrace(baseException));
+        if (mInvocationFailureDescription != null
+                && mInvocationFailureDescription.getFailureStatus() != null) {
+            debugBuilder.setFailureStatus(mInvocationFailureDescription.getFailureStatus());
+        }
+        DebugInfoContext.Builder debugContext = DebugInfoContext.newBuilder();
+        try {
+            debugContext.setErrorType(SerializationUtil.serializeToString(baseException));
+        } catch (IOException e) {
+            CLog.e("Failed to serialize the invocation failure:");
+            CLog.e(e);
+        }
+        debugBuilder.setDebugInfoContext(debugContext);
+
+        return debugBuilder.build();
+    }
 }
diff --git a/tests/src/com/android/tradefed/result/proto/ProtoResultReporterTest.java b/tests/src/com/android/tradefed/result/proto/ProtoResultReporterTest.java
index b3f0bfe..f4e3949 100644
--- a/tests/src/com/android/tradefed/result/proto/ProtoResultReporterTest.java
+++ b/tests/src/com/android/tradefed/result/proto/ProtoResultReporterTest.java
@@ -22,13 +22,18 @@
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.FailureDescription;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.LogFile;
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.result.proto.TestRecordProto.ChildReference;
+import com.android.tradefed.result.proto.TestRecordProto.DebugInfo;
+import com.android.tradefed.result.proto.TestRecordProto.DebugInfoContext;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.result.proto.TestRecordProto.TestRecord;
 import com.android.tradefed.result.proto.TestRecordProto.TestStatus;
 import com.android.tradefed.testtype.suite.ModuleDefinition;
+import com.android.tradefed.util.SerializationUtil;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
 import org.junit.Before;
@@ -145,6 +150,35 @@
         assertEquals("run failure", run1.getDebugInfo().getErrorMessage());
     }
 
+    /** Test an invocation with the proto invocation failure being populated. */
+    @Test
+    public void testInvocationFailure() throws Exception {
+        // Invocation start
+        mReporter.invocationStarted(mInvocationContext);
+        // Invocation ends
+        FailureDescription invocationFailure = FailureDescription.create("error");
+        invocationFailure.setFailureStatus(FailureStatus.INFRA_FAILURE);
+        invocationFailure.setCause(new NullPointerException("error"));
+        mReporter.invocationFailed(invocationFailure);
+        mReporter.invocationEnded(500L);
+
+        //  ------ Verify that everything was populated ------
+        assertNotNull(mFinalRecord.getTestRecordId());
+        assertNotNull(mFinalRecord.getStartTime().getSeconds());
+        assertNotNull(mFinalRecord.getEndTime().getSeconds());
+        assertNotNull(mFinalRecord.getDebugInfo());
+
+        DebugInfo invocFailure = mFinalRecord.getDebugInfo();
+        assertEquals("error", invocFailure.getErrorMessage());
+        assertEquals(FailureStatus.INFRA_FAILURE, invocFailure.getFailureStatus());
+        assertNotNull(invocFailure.getDebugInfoContext());
+        DebugInfoContext debugContext = invocFailure.getDebugInfoContext();
+        String serializedException = debugContext.getErrorType();
+        NullPointerException npe =
+                (NullPointerException) SerializationUtil.deserialize(serializedException);
+        assertEquals("error", npe.getMessage());
+    }
+
     /** Helper to create a module context. */
     private IInvocationContext createModuleContext(String moduleId) {
         IInvocationContext context = new InvocationContext();