Merge "Report unexecuted expected instrumentation tests"
diff --git a/src/com/android/tradefed/result/ConsoleResultReporter.java b/src/com/android/tradefed/result/ConsoleResultReporter.java
index 6cedd57..1955b7d 100644
--- a/src/com/android/tradefed/result/ConsoleResultReporter.java
+++ b/src/com/android/tradefed/result/ConsoleResultReporter.java
@@ -125,28 +125,33 @@
     public void invocationEnded(long elapsedTime) {
         int[] results = mResultCountListener.getResultCounts();
         StringBuilder sb = new StringBuilder();
+        sb.append("========== Result Summary ==========");
         sb.append(String.format("\nResults summary for test-tag '%s': ", mTestTag));
         sb.append(mResultCountListener.getTotalTests());
         sb.append(" Tests [");
         sb.append(results[TestStatus.PASSED.ordinal()]);
-        sb.append(" Passed ");
+        sb.append(" Passed");
         if (results[TestStatus.FAILURE.ordinal()] > 0) {
+            sb.append(" ");
             sb.append(results[TestStatus.FAILURE.ordinal()]);
-            sb.append(" Failed ");
+            sb.append(" Failed");
         }
         if (results[TestStatus.IGNORED.ordinal()] > 0) {
+            sb.append(" ");
             sb.append(results[TestStatus.IGNORED.ordinal()]);
-            sb.append(" Ignored ");
+            sb.append(" Ignored");
         }
         if (results[TestStatus.ASSUMPTION_FAILURE.ordinal()] > 0) {
+            sb.append(" ");
             sb.append(results[TestStatus.ASSUMPTION_FAILURE.ordinal()]);
-            sb.append(" Assumption failures ");
+            sb.append(" Assumption failures");
         }
         if (results[TestStatus.INCOMPLETE.ordinal()] > 0) {
+            sb.append(" ");
             sb.append(results[TestStatus.INCOMPLETE.ordinal()]);
             sb.append(" Incomplete");
         }
-        sb.append("\r\n");
+        sb.append("] \r\n");
         print(sb.toString());
         if (mDisplayFailureSummary) {
             for (Entry<TestDescription, TestResult> entry : mFailures.entrySet()) {
diff --git a/src/com/android/tradefed/result/proto/ProtoResultParser.java b/src/com/android/tradefed/result/proto/ProtoResultParser.java
index bb07623..5211aad 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultParser.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultParser.java
@@ -36,9 +36,11 @@
 import com.android.tradefed.result.proto.LogFileProto.LogFileInfo;
 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.TestRecord;
 import com.android.tradefed.testtype.suite.ModuleDefinition;
 import com.android.tradefed.util.MultiMap;
+import com.android.tradefed.util.SerializationUtil;
 import com.android.tradefed.util.proto.TestRecordProtoUtil;
 
 import com.google.common.base.Splitter;
@@ -274,9 +276,24 @@
         }
 
         if (endInvocationProto.hasDebugInfo()) {
-            // TODO: Re-interpret the exception with proper type.
             String trace = endInvocationProto.getDebugInfo().getTrace();
-            mListener.invocationFailed(new Throwable(trace));
+            Throwable invocationError = new Throwable(trace);
+            if (endInvocationProto.getDebugInfo().hasDebugInfoContext()) {
+                DebugInfoContext failureContext =
+                        endInvocationProto.getDebugInfo().getDebugInfoContext();
+                if (!Strings.isNullOrEmpty(failureContext.getErrorType())) {
+                    try {
+                        invocationError =
+                                (Throwable)
+                                        SerializationUtil.deserialize(
+                                                failureContext.getErrorType());
+                    } catch (IOException e) {
+                        CLog.e("Failed to deserialize the invocation exception:");
+                        CLog.e(e);
+                    }
+                }
+            }
+            mListener.invocationFailed(invocationError);
         }
 
         log("Invocation ended proto");
diff --git a/src/com/android/tradefed/result/proto/ProtoResultReporter.java b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
index 6ba026f..4e77622 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
@@ -33,12 +33,14 @@
 import com.android.tradefed.result.proto.TestRecordProto.TestStatus;
 import com.android.tradefed.result.retry.ISupportGranularResults;
 import com.android.tradefed.testtype.suite.ModuleDefinition;
+import com.android.tradefed.util.SerializationUtil;
 import com.android.tradefed.util.StreamUtil;
 
 import com.google.common.base.Strings;
 import com.google.protobuf.Any;
 import com.google.protobuf.Timestamp;
 
+import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Stack;
@@ -192,6 +194,14 @@
                 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);
         }
 
diff --git a/src/com/android/tradefed/sandbox/SandboxConfigDump.java b/src/com/android/tradefed/sandbox/SandboxConfigDump.java
index 6b6b7e8..2395bbb 100644
--- a/src/com/android/tradefed/sandbox/SandboxConfigDump.java
+++ b/src/com/android/tradefed/sandbox/SandboxConfigDump.java
@@ -106,10 +106,6 @@
                     // Ensure we get the stdout logging in FileLogger case.
                     ((FileLogger) logger).setLogLevelDisplay(LogLevel.VERBOSE);
                 }
-                // Turn off some of the invocation level options that would be duplicated in the
-                // parent.
-                config.getCommandOptions().setBugreportOnInvocationEnded(false);
-                config.getCommandOptions().setBugreportzOnInvocationEnded(false);
 
                 // Ensure in special conditions (placeholder devices) we can still allocate.
                 secureDeviceAllocation(config);
diff --git a/src/com/android/tradefed/sandbox/TradefedSandbox.java b/src/com/android/tradefed/sandbox/TradefedSandbox.java
index 83572ca..14e4f6f 100644
--- a/src/com/android/tradefed/sandbox/TradefedSandbox.java
+++ b/src/com/android/tradefed/sandbox/TradefedSandbox.java
@@ -378,6 +378,10 @@
                 }
                 throw e;
             }
+            // Turn off some of the invocation level options that would be duplicated in the
+            // child sandbox subprocess.
+            config.getCommandOptions().setBugreportOnInvocationEnded(false);
+            config.getCommandOptions().setBugreportzOnInvocationEnded(false);
         } catch (IOException | ConfigurationException e) {
             StreamUtil.close(mEventParser);
             StreamUtil.close(mProtoReceiver);
diff --git a/src/com/android/tradefed/util/SerializationUtil.java b/src/com/android/tradefed/util/SerializationUtil.java
index 0c991ff..0bd3fed 100644
--- a/src/com/android/tradefed/util/SerializationUtil.java
+++ b/src/com/android/tradefed/util/SerializationUtil.java
@@ -15,6 +15,8 @@
  */
 package com.android.tradefed.util;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -22,6 +24,7 @@
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
+import java.util.Base64;
 
 /** Utility to serialize/deserialize an object that implements {@link Serializable}. */
 public class SerializationUtil {
@@ -53,6 +56,49 @@
     }
 
     /**
+     * Serialize and object into a base64 encoded string.
+     *
+     * @param o the object to serialize.
+     * @return the {@link String} where the object was serialized.
+     * @throws IOException if serialization fails.
+     */
+    public static String serializeToString(Serializable o) throws IOException {
+        ByteArrayOutputStream byteOut = null;
+        ObjectOutputStream out = null;
+        try {
+            byteOut = new ByteArrayOutputStream();
+            out = new ObjectOutputStream(byteOut);
+            out.writeObject(o);
+            return Base64.getEncoder().encodeToString(byteOut.toByteArray());
+        } finally {
+            StreamUtil.close(out);
+            StreamUtil.close(byteOut);
+        }
+    }
+
+    /**
+     * Deserialize an object that was serialized using {@link #serializeToString(Serializable)}.
+     *
+     * @param serialized the base64 string where the object was serialized.
+     * @return the Object deserialized.
+     * @throws IOException if the deserialization fails.
+     */
+    public static Object deserialize(String serialized) throws IOException {
+        ByteArrayInputStream bais = null;
+        ObjectInputStream in = null;
+        try {
+            bais = new ByteArrayInputStream(Base64.getDecoder().decode(serialized));
+            in = new ObjectInputStream(bais);
+            return in.readObject();
+        } catch (ClassNotFoundException cnfe) {
+            throw new RuntimeException(cnfe);
+        } finally {
+            StreamUtil.close(in);
+            StreamUtil.close(bais);
+        }
+    }
+
+    /**
      * Deserialize an object that was serialized using {@link #serialize(Serializable)}.
      *
      * @param serializedFile the file where the object was serialized.
diff --git a/tests/src/com/android/tradefed/result/ConsoleResultReporterTest.java b/tests/src/com/android/tradefed/result/ConsoleResultReporterTest.java
index f1290f8..fd0a169 100644
--- a/tests/src/com/android/tradefed/result/ConsoleResultReporterTest.java
+++ b/tests/src/com/android/tradefed/result/ConsoleResultReporterTest.java
@@ -88,7 +88,7 @@
     public void testSummary() {
         mResultReporter.testResult(mTest, createTestResult(TestStatus.PASSED));
         mResultReporter.invocationEnded(0);
-        Truth.assertThat(mOutput.toString()).contains("1 Tests [1 Passed ");
+        Truth.assertThat(mOutput.toString()).contains("1 Tests [1 Passed]");
     }
 
     @Test
diff --git a/tests/src/com/android/tradefed/result/proto/ProtoResultParserTest.java b/tests/src/com/android/tradefed/result/proto/ProtoResultParserTest.java
index 517c0a0..eabcab2 100644
--- a/tests/src/com/android/tradefed/result/proto/ProtoResultParserTest.java
+++ b/tests/src/com/android/tradefed/result/proto/ProtoResultParserTest.java
@@ -17,6 +17,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tradefed.build.BuildInfo;
 import com.android.tradefed.config.ConfigurationDef;
@@ -166,7 +167,8 @@
         mMockListener.logAssociation(
                 EasyMock.eq("subprocess-invocation_log1"), EasyMock.anyObject());
         // Invocation failure is replayed
-        mMockListener.invocationFailed(EasyMock.anyObject());
+        Capture<Throwable> captureInvocFailure = new Capture<>();
+        mMockListener.invocationFailed(EasyMock.capture(captureInvocFailure));
         mMockListener.invocationEnded(500L);
 
         EasyMock.replay(mMockListener);
@@ -225,6 +227,9 @@
         assertEquals(logFile.getType(), capturedFile.getType());
         assertEquals(logFile.getSize(), capturedFile.getSize());
 
+        Throwable invocFailureCaptured = captureInvocFailure.getValue();
+        assertTrue(invocFailureCaptured instanceof RuntimeException);
+
         // Check Context at the end
         assertEquals(
                 "build_value", context.getBuildInfos().get(0).getBuildAttributes().get(TEST_KEY));
diff --git a/tests/src/com/android/tradefed/util/SerializationUtilTest.java b/tests/src/com/android/tradefed/util/SerializationUtilTest.java
index aab84cb..08a2f4f 100644
--- a/tests/src/com/android/tradefed/util/SerializationUtilTest.java
+++ b/tests/src/com/android/tradefed/util/SerializationUtilTest.java
@@ -15,7 +15,9 @@
  */
 package com.android.tradefed.util;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.android.tradefed.build.BuildInfo;
 import com.android.tradefed.build.BuildSerializedVersion;
@@ -48,6 +50,15 @@
         }
     }
 
+    @Test
+    public void testSerialize_DeserializeString() throws Exception {
+        RuntimeException e = new RuntimeException("test");
+        String serializedException = SerializationUtil.serializeToString(e);
+        Object o = SerializationUtil.deserialize(serializedException);
+        assertTrue(o instanceof RuntimeException);
+        assertEquals("test", ((RuntimeException) o).getMessage());
+    }
+
     /** Tests that serialization and deserialization creates a similar object from the original. */
     @Test
     public void testSerialize_Deserialize() throws Exception {