Add test module start/end to subprocess reporter

- Ensure Abi/IAbi is serializable so context can be passed.
- Add callbacks to properly carry the full state.

Test: unit tests
local google-cts-launcher run that reports module start/end
https://sponge.corp.google.com/invocation?id=99ece292-e5af-41e4-aa7b-ac6b50404d6a
Bug: None

Change-Id: I42eafba8c3d3ee6065d31d1ec9bb776a02e1392e
diff --git a/src/com/android/tradefed/result/SubprocessResultsReporter.java b/src/com/android/tradefed/result/SubprocessResultsReporter.java
index a73cbb8..118d58b 100644
--- a/src/com/android/tradefed/result/SubprocessResultsReporter.java
+++ b/src/com/android/tradefed/result/SubprocessResultsReporter.java
@@ -27,12 +27,15 @@
 import com.android.tradefed.util.SubprocessEventHelper.InvocationStartedEventInfo;
 import com.android.tradefed.util.SubprocessEventHelper.TestEndedEventInfo;
 import com.android.tradefed.util.SubprocessEventHelper.TestLogEventInfo;
+import com.android.tradefed.util.SubprocessEventHelper.TestModuleStartedEventInfo;
 import com.android.tradefed.util.SubprocessEventHelper.TestRunEndedEventInfo;
 import com.android.tradefed.util.SubprocessEventHelper.TestRunFailedEventInfo;
 import com.android.tradefed.util.SubprocessEventHelper.TestRunStartedEventInfo;
 import com.android.tradefed.util.SubprocessEventHelper.TestStartedEventInfo;
 import com.android.tradefed.util.SubprocessTestResultsParser;
 
+import org.json.JSONObject;
+
 import java.io.File;
 import java.io.FileWriter;
 import java.io.IOException;
@@ -203,6 +206,19 @@
         printEvent(SubprocessTestResultsParser.StatusKeys.INVOCATION_FAILED, info);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void testModuleStarted(IInvocationContext moduleContext) {
+        TestModuleStartedEventInfo info = new TestModuleStartedEventInfo(moduleContext);
+        printEvent(SubprocessTestResultsParser.StatusKeys.TEST_MODULE_STARTED, info);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void testModuleEnded() {
+        printEvent(SubprocessTestResultsParser.StatusKeys.TEST_MODULE_ENDED, new JSONObject());
+    }
+
     /**
      * {@inheritDoc}
      */
diff --git a/src/com/android/tradefed/testtype/Abi.java b/src/com/android/tradefed/testtype/Abi.java
index daa28ac..343c585 100644
--- a/src/com/android/tradefed/testtype/Abi.java
+++ b/src/com/android/tradefed/testtype/Abi.java
@@ -15,10 +15,13 @@
  */
 package com.android.tradefed.testtype;
 
+import com.android.tradefed.build.BuildSerializedVersion;
+
 /**
  * A class representing an ABI.
  */
 public class Abi implements IAbi {
+    private static final long serialVersionUID = BuildSerializedVersion.VERSION;
 
     private final String mName;
     private final String mBitness;
diff --git a/src/com/android/tradefed/testtype/IAbi.java b/src/com/android/tradefed/testtype/IAbi.java
index 5c5aec0..2a349e1 100644
--- a/src/com/android/tradefed/testtype/IAbi.java
+++ b/src/com/android/tradefed/testtype/IAbi.java
@@ -15,10 +15,10 @@
  */
 package com.android.tradefed.testtype;
 
-/**
- * Interface representing the ABI under test.
- */
-public interface IAbi {
+import java.io.Serializable;
+
+/** Interface representing the ABI under test. */
+public interface IAbi extends Serializable {
 
     /**
      * @return The name of the ABI.
diff --git a/src/com/android/tradefed/util/SubprocessEventHelper.java b/src/com/android/tradefed/util/SubprocessEventHelper.java
index 577477b..ffc0216 100644
--- a/src/com/android/tradefed/util/SubprocessEventHelper.java
+++ b/src/com/android/tradefed/util/SubprocessEventHelper.java
@@ -15,6 +15,7 @@
  */
 package com.android.tradefed.util;
 
+import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.LogDataType;
 
@@ -22,6 +23,7 @@
 import org.json.JSONObject;
 
 import java.io.File;
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.HashMap;
@@ -49,6 +51,8 @@
 
     private static final String TEST_TAG_KEY = "testTag";
 
+    private static final String MODULE_CONTEXT_KEY = "moduleContextFileName";
+
     /**
      * Helper for testRunStarted information
      */
@@ -419,4 +423,37 @@
             return tags.toString();
         }
     }
+
+    /** Helper for test module started information. */
+    public static class TestModuleStartedEventInfo {
+        public IInvocationContext mModuleContext;
+
+        public TestModuleStartedEventInfo(IInvocationContext moduleContext) {
+            mModuleContext = moduleContext;
+        }
+
+        public TestModuleStartedEventInfo(JSONObject jsonObject) throws JSONException {
+            String file = jsonObject.getString(MODULE_CONTEXT_KEY);
+            try {
+                mModuleContext =
+                        (IInvocationContext) SerializationUtil.deserialize(new File(file), true);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        public String toString() {
+            JSONObject tags = null;
+            try {
+                tags = new JSONObject();
+                File serializedContext = SerializationUtil.serialize(mModuleContext);
+                tags.put(MODULE_CONTEXT_KEY, serializedContext.getAbsolutePath());
+            } catch (IOException | JSONException e) {
+                CLog.e(e);
+                throw new RuntimeException(e);
+            }
+            return tags.toString();
+        }
+    }
 }
diff --git a/src/com/android/tradefed/util/SubprocessTestResultsParser.java b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
index d8398b3..ade8651 100644
--- a/src/com/android/tradefed/util/SubprocessTestResultsParser.java
+++ b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
@@ -27,6 +27,7 @@
 import com.android.tradefed.util.SubprocessEventHelper.InvocationStartedEventInfo;
 import com.android.tradefed.util.SubprocessEventHelper.TestEndedEventInfo;
 import com.android.tradefed.util.SubprocessEventHelper.TestLogEventInfo;
+import com.android.tradefed.util.SubprocessEventHelper.TestModuleStartedEventInfo;
 import com.android.tradefed.util.SubprocessEventHelper.TestRunEndedEventInfo;
 import com.android.tradefed.util.SubprocessEventHelper.TestRunFailedEventInfo;
 import com.android.tradefed.util.SubprocessEventHelper.TestRunStartedEventInfo;
@@ -61,6 +62,7 @@
 
     private ITestInvocationListener mListener;
     private TestIdentifier mCurrentTest = null;
+    private IInvocationContext mCurrentModuleContext = null;
     private Pattern mPattern = null;
     private Map<String, EventHandler> mHandlerMap = null;
     private EventReceiverThread mEventReceiver = null;
@@ -78,6 +80,8 @@
         public static final String TEST_RUN_ENDED = "TEST_RUN_ENDED";
         public static final String TEST_RUN_FAILED = "TEST_RUN_FAILED";
         public static final String TEST_RUN_STARTED = "TEST_RUN_STARTED";
+        public static final String TEST_MODULE_STARTED = "TEST_MODULE_STARTED";
+        public static final String TEST_MODULE_ENDED = "TEST_MODULE_ENDED";
         public static final String TEST_LOG = "TEST_LOG";
         public static final String INVOCATION_STARTED = "INVOCATION_STARTED";
     }
@@ -210,6 +214,8 @@
         sb.append(StatusKeys.TEST_RUN_ENDED).append("|");
         sb.append(StatusKeys.TEST_RUN_FAILED).append("|");
         sb.append(StatusKeys.TEST_RUN_STARTED).append("|");
+        sb.append(StatusKeys.TEST_MODULE_STARTED).append("|");
+        sb.append(StatusKeys.TEST_MODULE_ENDED).append("|");
         sb.append(StatusKeys.TEST_LOG).append("|");
         sb.append(StatusKeys.INVOCATION_STARTED);
         String patt = String.format("(.*)(%s)( )(.*)", sb.toString());
@@ -227,6 +233,8 @@
         mHandlerMap.put(StatusKeys.TEST_RUN_ENDED, new TestRunEndedEventHandler());
         mHandlerMap.put(StatusKeys.TEST_RUN_FAILED, new TestRunFailedEventHandler());
         mHandlerMap.put(StatusKeys.TEST_RUN_STARTED, new TestRunStartedEventHandler());
+        mHandlerMap.put(StatusKeys.TEST_MODULE_STARTED, new TestModuleStartedEventHandler());
+        mHandlerMap.put(StatusKeys.TEST_MODULE_ENDED, new TestModuleEndedEventHandler());
         mHandlerMap.put(StatusKeys.TEST_LOG, new TestLogEventHandler());
         mHandlerMap.put(StatusKeys.INVOCATION_STARTED, new InvocationStartedEventHandler());
     }
@@ -392,6 +400,27 @@
         }
     }
 
+    private class TestModuleStartedEventHandler implements EventHandler {
+        @Override
+        public void handleEvent(String eventJson) throws JSONException {
+            TestModuleStartedEventInfo module =
+                    new TestModuleStartedEventInfo(new JSONObject(eventJson));
+            mCurrentModuleContext = module.mModuleContext;
+            mListener.testModuleStarted(module.mModuleContext);
+        }
+    }
+
+    private class TestModuleEndedEventHandler implements EventHandler {
+        @Override
+        public void handleEvent(String eventJson) throws JSONException {
+            if (mCurrentModuleContext == null) {
+                CLog.w("Calling testModuleEnded when testModuleStarted was not called.");
+            }
+            mListener.testModuleEnded();
+            mCurrentModuleContext = null;
+        }
+    }
+
     private class TestLogEventHandler implements EventHandler {
         @Override
         public void handleEvent(String eventJson) throws JSONException {
diff --git a/tests/src/com/android/tradefed/util/SubprocessTestResultsParserTest.java b/tests/src/com/android/tradefed/util/SubprocessTestResultsParserTest.java
index 036ce2f..4d84ada 100644
--- a/tests/src/com/android/tradefed/util/SubprocessTestResultsParserTest.java
+++ b/tests/src/com/android/tradefed/util/SubprocessTestResultsParserTest.java
@@ -15,15 +15,22 @@
  */
 package com.android.tradefed.util;
 
+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.TestIdentifier;
+import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
 
-import junit.framework.TestCase;
-
 import org.easymock.Capture;
 import org.easymock.EasyMock;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -32,13 +39,11 @@
 import java.io.InputStreamReader;
 import java.io.PrintWriter;
 import java.net.Socket;
-import java.util.Map;
 import java.util.Vector;
 
-/**
- * Unit Tests for {@link SubprocessTestResultsParser}
- */
-public class SubprocessTestResultsParserTest extends TestCase {
+/** Unit Tests for {@link SubprocessTestResultsParser} */
+@RunWith(JUnit4.class)
+public class SubprocessTestResultsParserTest {
 
     private static final String TEST_TYPE_DIR = "testdata";
     private static final String SUBPROC_OUTPUT_FILE_1 = "subprocess1.txt";
@@ -70,10 +75,8 @@
         return fileContents.toArray(new String[fileContents.size()]);
     }
 
-    /**
-     * Tests the parser for cases of test failed, ignored, assumption failure
-     */
-    @SuppressWarnings("unchecked")
+    /** Tests the parser for cases of test failed, ignored, assumption failure */
+    @Test
     public void testParse_randomEvents() throws Exception {
         String[] contents = readInFile(SUBPROC_OUTPUT_FILE_1);
         ITestInvocationListener mockRunListener =
@@ -82,12 +85,9 @@
         mockRunListener.testStarted((TestIdentifier) EasyMock.anyObject(), EasyMock.anyLong());
         EasyMock.expectLastCall().times(4);
         mockRunListener.testEnded(
-                (TestIdentifier) EasyMock.anyObject(),
-                EasyMock.anyLong(),
-                (Map<String, String>) EasyMock.anyObject());
+                (TestIdentifier) EasyMock.anyObject(), EasyMock.anyLong(), EasyMock.anyObject());
         EasyMock.expectLastCall().times(4);
-        mockRunListener.testRunEnded(EasyMock.anyLong(),
-                (Map<String, String>) EasyMock.anyObject());
+        mockRunListener.testRunEnded(EasyMock.anyLong(), EasyMock.anyObject());
         EasyMock.expectLastCall().times(1);
         mockRunListener.testIgnored((TestIdentifier)EasyMock.anyObject());
         EasyMock.expectLastCall();
@@ -111,10 +111,8 @@
         }
     }
 
-    /**
-     * Tests the parser for cases of test starting without closing.
-     */
-    @SuppressWarnings("unchecked")
+    /** Tests the parser for cases of test starting without closing. */
+    @Test
     public void testParse_invalidEventOrder() throws Exception {
         String[] contents =  readInFile(SUBPROC_OUTPUT_FILE_2);
         ITestInvocationListener mockRunListener =
@@ -123,14 +121,11 @@
         mockRunListener.testStarted((TestIdentifier) EasyMock.anyObject(), EasyMock.anyLong());
         EasyMock.expectLastCall().times(4);
         mockRunListener.testEnded(
-                (TestIdentifier) EasyMock.anyObject(),
-                EasyMock.anyLong(),
-                (Map<String, String>) EasyMock.anyObject());
+                (TestIdentifier) EasyMock.anyObject(), EasyMock.anyLong(), EasyMock.anyObject());
         EasyMock.expectLastCall().times(3);
         mockRunListener.testRunFailed((String)EasyMock.anyObject());
         EasyMock.expectLastCall().times(1);
-        mockRunListener.testRunEnded(EasyMock.anyLong(),
-                (Map<String, String>) EasyMock.anyObject());
+        mockRunListener.testRunEnded(EasyMock.anyLong(), EasyMock.anyObject());
         EasyMock.expectLastCall().times(1);
         mockRunListener.testIgnored((TestIdentifier)EasyMock.anyObject());
         EasyMock.expectLastCall();
@@ -151,18 +146,14 @@
         }
     }
 
-    /**
-     * Tests the parser for cases of test starting without closing.
-     */
-    @SuppressWarnings("unchecked")
+    /** Tests the parser for cases of test starting without closing. */
+    @Test
     public void testParse_testNotStarted() throws Exception {
         ITestInvocationListener mockRunListener =
                 EasyMock.createMock(ITestInvocationListener.class);
         mockRunListener.testRunStarted("arm64-v8a CtsGestureTestCases", 4);
         mockRunListener.testEnded(
-                (TestIdentifier) EasyMock.anyObject(),
-                EasyMock.anyLong(),
-                (Map<String, String>) EasyMock.anyObject());
+                (TestIdentifier) EasyMock.anyObject(), EasyMock.anyLong(), EasyMock.anyObject());
         EasyMock.expectLastCall().times(1);
         EasyMock.replay(mockRunListener);
         File tmp = FileUtil.createTempFile("sub", "unit");
@@ -189,6 +180,7 @@
     }
 
     /** Tests the parser for a cases when there is no start/end time stamp. */
+    @Test
     public void testParse_noTimeStamp() throws Exception {
         ITestInvocationListener mockRunListener =
                 EasyMock.createMock(ITestInvocationListener.class);
@@ -224,10 +216,8 @@
         }
     }
 
-    /**
-     * Test injecting an invocation failure and verify the callback is called.
-     */
-    @SuppressWarnings("unchecked")
+    /** Test injecting an invocation failure and verify the callback is called. */
+    @Test
     public void testParse_invocationFailed() throws Exception {
         ITestInvocationListener mockRunListener =
                 EasyMock.createMock(ITestInvocationListener.class);
@@ -259,18 +249,14 @@
         }
     }
 
-    /**
-     * Report results when received from socket.
-     */
-    @SuppressWarnings("unchecked")
+    /** Report results when received from socket. */
+    @Test
     public void testParser_receiveFromSocket() throws Exception {
         ITestInvocationListener mockRunListener =
                 EasyMock.createMock(ITestInvocationListener.class);
         mockRunListener.testRunStarted("arm64-v8a CtsGestureTestCases", 4);
         mockRunListener.testEnded(
-                (TestIdentifier) EasyMock.anyObject(),
-                EasyMock.anyLong(),
-                (Map<String, String>) EasyMock.anyObject());
+                (TestIdentifier) EasyMock.anyObject(), EasyMock.anyLong(), EasyMock.anyObject());
         EasyMock.expectLastCall().times(1);
         EasyMock.replay(mockRunListener);
         SubprocessTestResultsParser resultParser = null;
@@ -303,10 +289,8 @@
         }
     }
 
-    /**
-     * When the receiver thread fails to join then an exception is thrown.
-     */
-    @SuppressWarnings("unchecked")
+    /** When the receiver thread fails to join then an exception is thrown. */
+    @Test
     public void testParser_failToJoin() throws Exception {
         ITestInvocationListener mockRunListener =
                 EasyMock.createMock(ITestInvocationListener.class);
@@ -323,6 +307,7 @@
     }
 
     /** Tests the parser receiving event on updating test tag. */
+    @Test
     public void testParse_testTag() throws Exception {
         final String subTestTag = "test_tag_in_subprocess";
         InvocationContext context = new InvocationContext();
@@ -351,6 +336,7 @@
     }
 
     /** Tests the parser should not overwrite the test tag in parent process if it's already set. */
+    @Test
     public void testParse_testTagNotOverwrite() throws Exception {
         final String subTestTag = "test_tag_in_subprocess";
         final String parentTestTag = "test_tag_in_parent_process";
@@ -374,4 +360,37 @@
             FileUtil.deleteFile(tmp);
         }
     }
+
+    /** Test that module start and end is properly parsed when reported. */
+    @Test
+    public void testParse_moduleStarted_end() throws Exception {
+        ITestInvocationListener mockRunListener =
+                EasyMock.createMock(ITestInvocationListener.class);
+        mockRunListener.testModuleStarted(EasyMock.anyObject());
+        mockRunListener.testModuleEnded();
+        EasyMock.replay(mockRunListener);
+        IInvocationContext fakeModuleContext = new InvocationContext();
+        File tmp = FileUtil.createTempFile("sub", "unit");
+        SubprocessTestResultsParser resultParser = null;
+        File serializedModule = null;
+        try {
+            serializedModule = SerializationUtil.serialize(fakeModuleContext);
+            resultParser =
+                    new SubprocessTestResultsParser(mockRunListener, new InvocationContext());
+            String moduleStart =
+                    String.format(
+                            "TEST_MODULE_STARTED {\"moduleContextFileName\":\"%s\"}\n",
+                            serializedModule.getAbsolutePath());
+            FileUtil.writeToFile(moduleStart, tmp, true);
+            String moduleEnd = "TEST_MODULE_ENDED {}\n";
+            FileUtil.writeToFile(moduleEnd, tmp, true);
+
+            resultParser.parseFile(tmp);
+            EasyMock.verify(mockRunListener);
+        } finally {
+            StreamUtil.close(resultParser);
+            FileUtil.deleteFile(tmp);
+            FileUtil.deleteFile(serializedModule);
+        }
+    }
 }