CTS: Add host-side EDI

Bug: 31042083
Change-Id: I131f5831bf4448d2fd2ada4285e6c2fb2c0b5180
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/targetprep/DeviceInfoCollector.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/targetprep/DeviceInfoCollector.java
index dd91d11..08055a3 100644
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/targetprep/DeviceInfoCollector.java
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/targetprep/DeviceInfoCollector.java
@@ -18,6 +18,7 @@
 
 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
 import com.android.compatibility.common.tradefed.testtype.CompatibilityTest;
+import com.android.compatibility.common.tradefed.util.CollectorUtil;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -26,6 +27,7 @@
 import com.android.tradefed.targetprep.BuildError;
 import com.android.tradefed.targetprep.TargetSetupError;
 import com.android.tradefed.util.ArrayUtil;
+import com.android.tradefed.util.FileUtil;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -74,8 +76,17 @@
     @Option(name = "dest-dir", description = "The directory under the result to store the files")
     private String mDestDir;
 
+    @Option(name = "temp-dir", description = "The directory containing host-side device info files")
+    private String mTempDir;
+
+    // Temp directory for host-side device info files.
+    private File mHostDir;
+
+    // Destination result directory for all device info files.
+    private File mResultDir;
+
     public DeviceInfoCollector() {
-        mWhen = When.BEFORE;
+        mWhen = When.BOTH;
     }
 
     @Override
@@ -88,39 +99,57 @@
         if (mSkipDeviceInfo) {
             return;
         }
+
+        createTempHostDir();
+        createResultDir(buildInfo);
         run(device, buildInfo);
-        getDeviceInfoFiles(device, buildInfo);
+        getDeviceInfoFiles(device);
     }
 
-    private void getDeviceInfoFiles(ITestDevice device, IBuildInfo buildInfo) {
-        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(buildInfo);
+    @Override
+    public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e) {
+        if (mSkipDeviceInfo) {
+            return;
+        }
+        if (mHostDir != null && mHostDir.isDirectory() &&
+                    mResultDir != null && mResultDir.isDirectory()) {
+            CollectorUtil.pullFromHost(mHostDir, mResultDir);
+        }
+    }
+
+    private void createTempHostDir() {
         try {
-            File resultDir = buildHelper.getResultDir();
-            if (mDestDir != null) {
-                resultDir = new File(resultDir, mDestDir);
-            }
-            resultDir.mkdirs();
-            if (!resultDir.isDirectory()) {
-                CLog.e("%s is not a directory", resultDir.getAbsolutePath());
+            mHostDir = FileUtil.createNamedTempDir(mTempDir);
+            if (!mHostDir.isDirectory()) {
+                CLog.e("%s is not a directory", mHostDir.getAbsolutePath());
                 return;
             }
-            String resultPath = resultDir.getAbsolutePath();
-            pull(device, mSrcDir, resultPath);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void createResultDir(IBuildInfo buildInfo) {
+        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(buildInfo);
+        try {
+            mResultDir = buildHelper.getResultDir();
+            if (mDestDir != null) {
+                mResultDir = new File(mResultDir, mDestDir);
+            }
+            mResultDir.mkdirs();
+            if (!mResultDir.isDirectory()) {
+                CLog.e("%s is not a directory", mResultDir.getAbsolutePath());
+                return;
+            }
         } catch (FileNotFoundException fnfe) {
             fnfe.printStackTrace();
         }
     }
 
-    private void pull(ITestDevice device, String src, String dest) {
-        String command = String.format("adb -s %s pull %s %s", device.getSerialNumber(), src, dest);
-        try {
-            Process p = Runtime.getRuntime().exec(new String[] {"/bin/bash", "-c", command});
-            if (p.waitFor() != 0) {
-                CLog.e("Failed to run %s", command);
-            }
-        } catch (Exception e) {
-            CLog.e("Caught exception during pull.");
-            CLog.e(e);
+    private void getDeviceInfoFiles(ITestDevice device) {
+        if (mResultDir != null && mResultDir.isDirectory()) {
+            String mResultPath = mResultDir.getAbsolutePath();
+            CollectorUtil.pullFromDevice(device, mSrcDir, mResultPath);
         }
     }
 }
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/util/CollectorUtil.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/util/CollectorUtil.java
new file mode 100644
index 0000000..a664515
--- /dev/null
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/util/CollectorUtil.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2016 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.compatibility.common.tradefed.util;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Util class for {@link ReportLogCollector} and {@link DeviceInfoCollector}.
+ */
+public class CollectorUtil {
+
+    private CollectorUtil() {
+    }
+
+    private static final String ADB_LS_PATTERN = "([^\\s]+)\\s*";
+    private static final String TEST_METRICS_PATTERN = "\\\"([a-z0-9_]*)\\\":(\\{[^{}]*\\})";
+
+    /**
+     * Copy files from device to host.
+     * @param device The device reference.
+     * @param src The source directory on the device.
+     * @param dest The destination directory.
+     */
+    public static void pullFromDevice(ITestDevice device, String src, String dest) {
+        try {
+            if (device.doesFileExist(src)) {
+                String listCommand = String.format("ls %s", src);
+                String fileList = device.executeShellCommand(listCommand);
+                Pattern p = Pattern.compile(ADB_LS_PATTERN);
+                Matcher m = p.matcher(fileList);
+                while (m.find()) {
+                    String fileName = m.group(1);
+                    String srcPath = String.format("%s%s", src, fileName);
+                    File destFile = new File(String.format("%s/%s", dest, fileName));
+                    device.pullFile(srcPath, destFile);
+                }
+            }
+        } catch (DeviceNotAvailableException e) {
+            CLog.e("Caught exception during pull.");
+            CLog.e(e);
+        }
+    }
+
+    /**
+     * Copy files from host and delete from source.
+     * @param src The source directory.
+     * @param dest The destination directory.
+     */
+    public static void pullFromHost(File src, File dest) {
+        try {
+            FileUtil.recursiveCopy(src, dest);
+            FileUtil.recursiveDelete(src);
+        } catch (IOException e) {
+            CLog.e("Caught exception during pull.");
+            CLog.e(e);
+        }
+    }
+
+    /**
+     * Reformat test metrics jsons to convert multiple json objects with identical stream names into
+     * arrays of objects (b/28790467).
+     *
+     * @param resultDir The directory containing test metrics.
+     */
+    public static void reformatRepeatedStreams(File resultDir) {
+        try {
+            File[] reportLogs = resultDir.listFiles();
+            for (File reportLog : reportLogs) {
+                writeFile(reportLog, reformatJsonString(readFile(reportLog)));
+            }
+        } catch (IOException e) {
+            CLog.e("Caught exception during reformatting.");
+            CLog.e(e);
+        }
+    }
+
+    /**
+     * Helper function to read a file.
+     *
+     * @throws IOException
+     */
+    private static String readFile(File file) throws IOException {
+        StringBuilder stringBuilder = new StringBuilder();
+        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                stringBuilder.append(line);
+            }
+        }
+        return stringBuilder.toString();
+    }
+
+    /**
+     * Helper function to write to a file.
+     *
+     * @param file {@link File} to write to.
+     * @param jsonString String to be written.
+     * @throws IOException
+     */
+    private static void writeFile(File file, String jsonString) throws IOException {
+        file.createNewFile();
+        try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
+            writer.write(jsonString, 0, jsonString.length());
+        }
+    }
+
+    /**
+     * Helper function to reformat JSON string.
+     *
+     * @param jsonString
+     * @return
+     */
+    public static String reformatJsonString(String jsonString) {
+        StringBuilder newJsonBuilder = new StringBuilder();
+        // Create map of stream names and json objects.
+        HashMap<String, List<String>> jsonMap = new HashMap<>();
+        Pattern p = Pattern.compile(TEST_METRICS_PATTERN);
+        Matcher m = p.matcher(jsonString);
+        while (m.find()) {
+            String key = m.group(1);
+            String value = m.group(2);
+            if (!jsonMap.containsKey(key)) {
+                jsonMap.put(key, new ArrayList<String>());
+            }
+            jsonMap.get(key).add(value);
+        }
+        // Rewrite json string as arrays.
+        newJsonBuilder.append("{");
+        boolean firstLine = true;
+        for (String key : jsonMap.keySet()) {
+            if (!firstLine) {
+                newJsonBuilder.append(",");
+            } else {
+                firstLine = false;
+            }
+            newJsonBuilder.append("\"").append(key).append("\":[");
+            boolean firstValue = true;
+            for (String stream : jsonMap.get(key)) {
+                if (!firstValue) {
+                    newJsonBuilder.append(",");
+                } else {
+                    firstValue = false;
+                }
+                newJsonBuilder.append(stream);
+            }
+            newJsonBuilder.append("]");
+        }
+        newJsonBuilder.append("}");
+        return newJsonBuilder.toString();
+    }
+}
\ No newline at end of file
diff --git a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/UnitTests.java b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/UnitTests.java
index b710128..dfe67c1 100644
--- a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/UnitTests.java
+++ b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/UnitTests.java
@@ -25,6 +25,7 @@
 import com.android.compatibility.common.tradefed.testtype.ModuleDefTest;
 import com.android.compatibility.common.tradefed.testtype.ModuleRepoTest;
 import com.android.compatibility.common.tradefed.util.OptionHelperTest;
+import com.android.compatibility.common.tradefed.util.CollectorUtilTest;
 
 import junit.framework.Test;
 import junit.framework.TestSuite;
@@ -45,6 +46,7 @@
         addTestSuite(ResultReporterTest.class);
         addTestSuite(CompatibilityTestTest.class);
         addTestSuite(OptionHelperTest.class);
+        addTestSuite(CollectorUtilTest.class);
         addTestSuite(ModuleDefTest.class);
         addTestSuite(ModuleRepoTest.class);
         addTestSuite(PropertyCheckTest.class);
diff --git a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/util/CollectorUtilTest.java b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/util/CollectorUtilTest.java
new file mode 100644
index 0000000..d5b71fa
--- /dev/null
+++ b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/util/CollectorUtilTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 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.compatibility.common.tradefed.util;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link CollectorUtil}
+ */
+public class CollectorUtilTest extends TestCase {
+
+    String UNFORMATTED_JSON = "{"
+            + "\"stream_name_1\":"
+            + "{\"id\":1,\"key1\":\"value1\"},"
+            + "\"stream_name_2\":"
+            + "{\"id\":1,\"key1\":\"value3\"},"
+            + "\"stream_name_1\":"
+            + "{\"id\":2,\"key1\":\"value2\"},"
+            + "}";
+
+    String REFORMATTED_JSON = "{"
+            + "\"stream_name_2\":"
+            + "["
+            + "{\"id\":1,\"key1\":\"value3\"}"
+            + "],"
+            + "\"stream_name_1\":"
+            + "["
+            + "{\"id\":1,\"key1\":\"value1\"},"
+            + "{\"id\":2,\"key1\":\"value2\"}"
+            + "]"
+            + "}";
+
+    public void testReformatJsonString() throws Exception {
+        String reformattedJson = CollectorUtil.reformatJsonString(UNFORMATTED_JSON);
+        assertEquals(reformattedJson, REFORMATTED_JSON);
+    }
+
+}
\ No newline at end of file
diff --git a/common/host-side/util/src/com/android/compatibility/common/util/DeviceInfo.java b/common/host-side/util/src/com/android/compatibility/common/util/DeviceInfo.java
new file mode 100644
index 0000000..ef05813
--- /dev/null
+++ b/common/host-side/util/src/com/android/compatibility/common/util/DeviceInfo.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 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.compatibility.common.util;
+
+import com.android.compatibility.common.util.HostInfoStore;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+
+/**
+ * Collect device information from host and write to a JSON file.
+ */
+public abstract class DeviceInfo extends DeviceTestCase {
+
+    // Temporary folder must match the temp-dir value configured in DeviceInfoCollector target
+    // preparer in cts/tools/cts-tradefed/res/config/cts-preconditions.xml
+    private static final String TEMPORARY_REPORT_FOLDER = "temp-device-info-files/";
+
+    private HostInfoStore mStore;
+
+    public void testCollectDeviceInfo() throws Exception {
+        String collectionName = getClass().getSimpleName();
+        try {
+            final File dir = FileUtil.createNamedTempDir(TEMPORARY_REPORT_FOLDER);
+            File jsonFile = new File(dir, collectionName + ".deviceinfo.json");
+            mStore = new HostInfoStore(jsonFile);
+            mStore.open();
+            collectDeviceInfo(mStore);
+            mStore.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+            fail(String.format("Failed to collect device info (%s): %s",
+                    collectionName, e.getMessage()));
+        }
+    }
+
+    /**
+     * Method to collect device information.
+     */
+    protected abstract void collectDeviceInfo(HostInfoStore store) throws Exception;
+}
\ No newline at end of file
diff --git a/hostsidetests/sample/src/android/sample/cts/SampleHostDeviceInfo.java b/hostsidetests/sample/src/android/sample/cts/SampleHostDeviceInfo.java
new file mode 100644
index 0000000..aa5214e
--- /dev/null
+++ b/hostsidetests/sample/src/android/sample/cts/SampleHostDeviceInfo.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 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 android.sample.cts;
+
+import com.android.compatibility.common.util.DeviceInfo;
+import com.android.compatibility.common.util.HostInfoStore;
+import com.android.tradefed.device.ITestDevice;
+
+import java.util.Arrays;
+
+public class SampleHostDeviceInfo extends DeviceInfo {
+
+    private ITestDevice mDevice;
+
+    @Override
+    protected void collectDeviceInfo(HostInfoStore store) throws Exception {
+
+        mDevice = getDevice();
+
+        store.startGroup("product");
+        store.addResult("model", getProperty("ro.product.model"));
+        store.addResult("brand", getProperty("ro.product.brand"));
+        store.addResult("name", getProperty("ro.product.name"));
+        store.addResult("device", getProperty("ro.product.device"));
+        store.addResult("board", getProperty("ro.product.board"));
+
+        String abi = getProperty("ro.product.cpu.abilist");
+        store.addListResult("abi", Arrays.asList(abi.split(",")));
+        store.endGroup(); // product
+
+        store.startGroup("version");
+        store.addResult("sdk", getProperty("ro.build.version.sdk"));
+        store.addResult("codename", getProperty("ro.build.version.codename"));
+        store.addResult("security_patch", getProperty("ro.build.version.security_patch"));
+        store.addResult("base_os", getProperty("ro.build.version.base_os"));
+        store.endGroup(); // version
+    }
+
+    private String getProperty(String prop) throws Exception {
+        return mDevice.executeShellCommand("getprop " + prop).replace("\n", "");
+    }
+}
\ No newline at end of file
diff --git a/tools/cts-tradefed/res/config/cts-preconditions.xml b/tools/cts-tradefed/res/config/cts-preconditions.xml
index 4121976..3430576 100644
--- a/tools/cts-tradefed/res/config/cts-preconditions.xml
+++ b/tools/cts-tradefed/res/config/cts-preconditions.xml
@@ -52,6 +52,7 @@
         <option name="package" value="com.android.compatibility.common.deviceinfo"/>
         <option name="src-dir" value="/sdcard/device-info-files/"/>
         <option name="dest-dir" value="device-info-files/"/>
+        <option name="temp-dir" value="temp-device-info-files/"/>
     </target_preparer>
 
     <!-- The following values are used in cts/common/device-side/util/DeviceReportLog.java,