Add a listener to communicate with a test.

If EVENT_FILE_ROBOLECTRIC exist, create AtestRunListener to inform atest about test status.

Bug: 124421998

Test: source build/envsetup.sh ; lunch
      m atest && atest-dev RunStorageManagerRoboTests RunBluetoothRoboTests

Change-Id: I9b8f9d4df6e1d9b68f642ac1060755db5c6f34d8
diff --git a/libraries/junitxml/src/com/android/junitxml/AtestRunListener.java b/libraries/junitxml/src/com/android/junitxml/AtestRunListener.java
new file mode 100644
index 0000000..67f0dd0
--- /dev/null
+++ b/libraries/junitxml/src/com/android/junitxml/AtestRunListener.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.junitxml;
+
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * {@link RunListener} to write JUnit4 test status to File, then atest could use this file to get
+ * real time status}.
+ */
+public class AtestRunListener extends RunListener {
+
+    private static final String CLASSNAME_KEY = "className";
+    private static final String TESTNAME_KEY = "testName";
+    private static final String TRACE_KEY = "trace";
+    private static final String RUNNAME_KEY = "runName";
+    private static final String TESTCOUNT_KEY = "testCount";
+    private static final String ATTEMPT_KEY = "runAttempt";
+    private static final String TIME_KEY = "time";
+    private static final String START_TIME_KEY = "start_time";
+    private static final String END_TIME_KEY = "end_time";
+    private static final String MODULE_NAME_KEY = "moduleName";
+
+    /** Relevant test status keys. */
+    public static class StatusKeys {
+        public static final String TEST_ENDED = "TEST_ENDED";
+        public static final String TEST_FAILED = "TEST_FAILED";
+        public static final String TEST_IGNORED = "TEST_IGNORED";
+        public static final String TEST_STARTED = "TEST_STARTED";
+        public static final String TEST_RUN_ENDED = "TEST_RUN_ENDED";
+        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";
+    }
+
+    private final int mTotalCount;
+
+    // the file where to log the events.
+    private final File mReportFile;
+
+    private final String mSuiteName;
+
+    public AtestRunListener(String suiteName, File reportFile, int totalCount) {
+        mSuiteName = suiteName;
+        mReportFile = reportFile;
+        mTotalCount = totalCount;
+    }
+
+    @Override
+    public void testRunStarted(Description description) {
+        Map<String, Object> moduleStartEventData = new HashMap<>();
+        moduleStartEventData.put(MODULE_NAME_KEY, mSuiteName);
+        printEvent(StatusKeys.TEST_MODULE_STARTED, moduleStartEventData);
+
+        Map<String, Object> testRunEventData = new HashMap<>();
+        testRunEventData.put(TESTCOUNT_KEY, mTotalCount);
+        testRunEventData.put(ATTEMPT_KEY, 0);
+        testRunEventData.put(RUNNAME_KEY, mSuiteName);
+        printEvent(StatusKeys.TEST_RUN_STARTED, testRunEventData);
+    }
+
+    @Override
+    public void testRunFinished(Result result) {
+        Map<String, Object> eventData = new HashMap<>();
+        eventData.put(TIME_KEY, result.getRunTime());
+        printEvent(StatusKeys.TEST_RUN_ENDED, eventData);
+        printEvent(StatusKeys.TEST_MODULE_ENDED, new HashMap<>());
+    }
+
+    @Override
+    public void testFailure(Failure failure) {
+        Description description = failure.getDescription();
+        Map<String, Object> eventData = new HashMap<>();
+        eventData.put(CLASSNAME_KEY, description.getClassName());
+        eventData.put(TESTNAME_KEY, description.getMethodName());
+        eventData.put(TRACE_KEY, failure.getTrace());
+        printEvent(StatusKeys.TEST_FAILED, eventData);
+    }
+
+    @Override
+    public void testStarted(Description description) {
+        Map<String, Object> eventData = new HashMap<>();
+        eventData.put(START_TIME_KEY, System.currentTimeMillis());
+        eventData.put(CLASSNAME_KEY, description.getClassName());
+        eventData.put(TESTNAME_KEY, description.getMethodName());
+        printEvent(StatusKeys.TEST_STARTED, eventData);
+    }
+
+    @Override
+    public void testFinished(Description description) {
+        Map<String, Object> eventData = new HashMap<>();
+        eventData.put(END_TIME_KEY, System.currentTimeMillis());
+        eventData.put(CLASSNAME_KEY, description.getClassName());
+        eventData.put(TESTNAME_KEY, description.getMethodName());
+        printEvent(StatusKeys.TEST_ENDED, eventData);
+    }
+
+    @Override
+    public void testIgnored(Description description) {
+        Map<String, Object> eventData = new HashMap<>();
+        eventData.put(TESTNAME_KEY, description.getMethodName());
+        eventData.put(CLASSNAME_KEY, description.getClassName());
+        eventData.put(START_TIME_KEY, System.currentTimeMillis());
+        eventData.put(END_TIME_KEY, System.currentTimeMillis());
+        printEvent(StatusKeys.TEST_STARTED, eventData);
+        printEvent(StatusKeys.TEST_IGNORED, eventData);
+        printEvent(StatusKeys.TEST_ENDED, eventData);
+    }
+
+    private void printEvent(String key, Map<String, Object> event) {
+        if (mReportFile.canWrite()) {
+            try {
+                try (FileWriter fw = new FileWriter(mReportFile, true)) {
+                    String eventLog = String.format("%s %s\n\n", key, toJson(event));
+                    fw.append(eventLog);
+                }
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        } else {
+            throw new RuntimeException(
+                    String.format(
+                            "report file: %s is not writable", mReportFile.getAbsolutePath()));
+        }
+    }
+
+    private String toJson(Map<String, Object> data) {
+        StringBuilder sb = new StringBuilder();
+        for (Map.Entry<String, Object> entry : data.entrySet()) {
+            sb.append(",");
+            sb.append("\"").append(entry.getKey()).append("\":");
+            if (entry.getValue() instanceof Number) {
+                sb.append(entry.getValue().toString());
+            } else {
+                sb.append("\"").append(entry.getValue()).append("\"");
+            }
+        }
+        sb.replace(0, 1, "{").append("}");
+        return sb.toString();
+    }
+}
diff --git a/libraries/junitxml/src/com/android/junitxml/JUnitXmlRunner.java b/libraries/junitxml/src/com/android/junitxml/JUnitXmlRunner.java
index 82f1c1f..70f602d 100644
--- a/libraries/junitxml/src/com/android/junitxml/JUnitXmlRunner.java
+++ b/libraries/junitxml/src/com/android/junitxml/JUnitXmlRunner.java
@@ -16,13 +16,16 @@
 
 package com.android.junitxml;
 
+import org.junit.Test;
 import org.junit.internal.TextListener;
 import org.junit.runner.JUnitCore;
 import org.junit.runner.Result;
 
+import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.lang.reflect.Method;
 import java.util.stream.Stream;
 
 /**
@@ -54,24 +57,45 @@
         return null;
     }
 
+    private static AtestRunListener getAtestRunListener(int count) {
+        String outputFileStr = System.getenv("EVENT_FILE_ROBOLECTRIC");
+        String suiteName = System.getenv("TEST_WORKSPACE");
+        if (outputFileStr != null && outputFileStr.length() > 0) {
+            File outputFile = new File(outputFileStr);
+            if (outputFile.exists()) {
+                return new AtestRunListener(suiteName, outputFile, count);
+            }
+        }
+        return null;
+    }
+
     public static void main(String... args) {
         JUnitCore core = new JUnitCore();
         try {
+            Class[] as =
+                    Stream.of(args)
+                            .map(
+                                    test -> {
+                                        try {
+                                            return Class.forName(test);
+                                        } catch (ClassNotFoundException e) {
+                                            throw new RuntimeException(e);
+                                        }
+                                    })
+                            .toArray(Class[]::new);
             TextListener textListener = new TextListener(System.out);
             core.addListener(textListener);
             XmlRunListener xmlListener = getRunListener();
             if (xmlListener != null) {
                 core.addListener(xmlListener);
             }
-            Result result = core.run(Stream.of(args)
-                    .map(test -> {
-                        try {
-                            return Class.forName(test);
-                        } catch (ClassNotFoundException e) {
-                            throw new RuntimeException(e);
-                        }
-                    })
-                    .toArray(Class[]::new));
+
+            // Add AtestRunListener to communicate with ATest.
+            AtestRunListener atestRunListener = getAtestRunListener(calcTestCount(as));
+            if (atestRunListener != null) {
+                core.addListener(atestRunListener);
+            }
+            Result result = core.run(as);
             if (xmlListener != null) {
                 xmlListener.endTestSuite();
             }
@@ -80,5 +104,18 @@
             throw new RuntimeException(e);
         }
     }
+
+    private static int calcTestCount(Class[] as) {
+        int count = 0;
+        for (Class cls : as) {
+            Method[] declaredMethods = cls.getMethods();
+            for (Method method : declaredMethods) {
+                if (method.isAnnotationPresent(Test.class)) {
+                    count++;
+                }
+            }
+        }
+        return count;
+    }
 }