Refactor ResultReporter to facilitate other tests suites extending it and customizing the build info.

Bug: 64458205

Test: Ran existing unit tests and cts invocation and retry session.
cts-tradefed run cts
cts-tradefed run cts -r 0
run_unit_tests.sh

Change-Id: I35fe8d85f09f2e8103c0a5007061a9aaf189b22e
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/ResultReporter.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/ResultReporter.java
index 7b64881..3fedf00 100644
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/ResultReporter.java
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/ResultReporter.java
@@ -64,6 +64,7 @@
 import java.io.InputStream;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -83,9 +84,14 @@
     private static final String CTS_PREFIX = "cts:";
     private static final String BUILD_INFO = CTS_PREFIX + "build_";
 
-    /**
-     * List of files and directories that should NOT be copied when retrying CTS.
-     */
+    public static final String BUILD_VERSION_RELEASE = "build_version_release";
+    public static final String BUILD_ID = "build_id";
+    public static final String BUILD_PRODUCT = "build_product";
+    public static final String BUILD_DEVICE = "build_device";
+    public static final String BUILD_MANUFACTURER = "build_manufacturer";
+    public static final String BUILD_BRAND = "build_brand";
+    public static final String BUILD_FINGERPRINT = "build_fingerprint";
+
     private static final List<String> NOT_RETRY_FILES = Arrays.asList(
             ChecksumReporter.NAME,
             ChecksumReporter.PREV_NAME,
@@ -129,7 +135,7 @@
     private int invocationEndedCount = 0;
     private CountDownLatch mFinalized = null;
 
-    private IInvocationResult mResult = new InvocationResult();
+    protected IInvocationResult mResult = new InvocationResult();
     private IModuleResult mCurrentModuleResult;
     private ICaseResult mCurrentCaseResult;
     private ITestResult mCurrentResult;
@@ -158,6 +164,9 @@
 
     private LogFileSaver mTestLogSaver;
 
+    // Elapsed time from invocation started to ended.
+    private long mElapsedTime;
+
     /**
      * Default constructor.
      */
@@ -460,27 +469,25 @@
             if (++invocationEndedCount < mMasterBuildInfos.size()) {
                 return;
             }
-            finalizeResults(elapsedTime);
+            mElapsedTime = elapsedTime;
+            finalizeResults();
             mFinalized.countDown();
         }
     }
 
-    private void finalizeResults(long elapsedTime) {
+    private void finalizeResults() {
         // Add all device serials into the result to be serialized
         for (String deviceSerial : mMasterDeviceSerials) {
             mResult.addDeviceSerial(deviceSerial);
         }
 
+        addDeviceBuildInfoToResult();
+
         Set<String> allExpectedModules = new HashSet<>();
-        // Add all build info to the result to be serialized
         for (IBuildInfo buildInfo : mMasterBuildInfos) {
             for (Map.Entry<String, String> entry : buildInfo.getBuildAttributes().entrySet()) {
                 String key = entry.getKey();
                 String value = entry.getValue();
-                if (key.startsWith(BUILD_INFO)) {
-                    mResult.addInvocationInfo(key.substring(CTS_PREFIX.length()), value);
-                }
-
                 if (key.equals(CompatibilityBuildHelper.MODULE_IDS) && value.length() > 0) {
                     Collections.addAll(allExpectedModules, value.split(","));
                 }
@@ -496,17 +503,13 @@
         String moduleProgress = String.format("%d of %d",
                 mResult.getModuleCompleteCount(), mResult.getModules().size());
 
-        long startTime = mResult.getStartTime();
+
         try {
             // Zip the full test results directory.
             copyDynamicConfigFiles();
             copyFormattingFiles(mResultDir, mBuildHelper.getSuiteName());
 
-            File resultFile = ResultHandler.writeResults(mBuildHelper.getSuiteName(),
-                    mBuildHelper.getSuiteVersion(), mBuildHelper.getSuitePlan(),
-                    mBuildHelper.getSuiteBuild(), mResult, mResultDir, startTime,
-                    elapsedTime + startTime, mReferenceUrl, getLogUrl(),
-                    mBuildHelper.getCommandLineArgs());
+            File resultFile = generateResultXmlFile();
             if (mRetrySessionId != null) {
                 copyRetryFiles(ResultHandler.getResultDirectory(
                         mBuildHelper.getResultsDir(), mRetrySessionId), mResultDir);
@@ -532,7 +535,7 @@
         }
         // print the run results last.
         info("Invocation finished in %s. PASSED: %d, FAILED: %d, MODULES: %s",
-                TimeUtil.formatElapsedTime(elapsedTime),
+                TimeUtil.formatElapsedTime(mElapsedTime),
                 mResult.countResults(TestStatus.PASS),
                 mResult.countResults(TestStatus.FAIL),
                 moduleProgress);
@@ -658,6 +661,82 @@
     }
 
     /**
+     * Create results file compatible with CTSv2 (xml) report format.
+     */
+    protected File generateResultXmlFile()
+            throws IOException, XmlPullParserException {
+        return ResultHandler.writeResults(mBuildHelper.getSuiteName(),
+                mBuildHelper.getSuiteVersion(), mBuildHelper.getSuitePlan(),
+                mBuildHelper.getSuiteBuild(), mResult, mResultDir, mResult.getStartTime(),
+                mElapsedTime + mResult.getStartTime(), mReferenceUrl, getLogUrl(),
+                mBuildHelper.getCommandLineArgs());
+    }
+
+    /**
+     * Add build info collected from the device attributes to the results.
+     */
+    protected void addDeviceBuildInfoToResult() {
+        // Add all build info to the result to be serialized
+        Map<String, String> buildProperties = mapBuildInfo();
+        addBuildInfoToResult(buildProperties, mResult);
+    }
+
+    /**
+     * Override specific build properties so the report will be associated with the
+     * build fingerprint being certified.
+     */
+    protected void addDeviceBuildInfoToResult(String buildFingerprintOverride,
+            String manufactureOverride) {
+
+        Map<String, String> buildProperties = mapBuildInfo();
+
+        // Extract and override values from build fingerprint.
+        // Build fingerprint format: brand/product/device:version/build_id/tags
+        String fingerprintPrefix = buildFingerprintOverride.split(":")[0];
+        String fingerprintTail = buildFingerprintOverride.split(":")[1];
+        String buildIdOverride = fingerprintTail.split("/")[1];
+        buildProperties.put(BUILD_ID, buildIdOverride);
+        String brandOverride = fingerprintPrefix.split("/")[0];
+        buildProperties.put(BUILD_BRAND, brandOverride);
+        String deviceOverride = fingerprintPrefix.split("/")[2];
+        buildProperties.put(BUILD_DEVICE, deviceOverride);
+        String productOverride = fingerprintPrefix.split("/")[1];
+        buildProperties.put(BUILD_PRODUCT, productOverride);
+        String versionOverride = fingerprintTail.split("/")[0];
+        buildProperties.put(BUILD_VERSION_RELEASE, versionOverride);
+        buildProperties.put(BUILD_FINGERPRINT, buildFingerprintOverride);
+        buildProperties.put(BUILD_MANUFACTURER, manufactureOverride);
+
+        // Add modified values to results.
+        addBuildInfoToResult(buildProperties, mResult);
+        mResult.setBuildFingerprint(buildFingerprintOverride);
+    }
+    /** Aggregate build info from member device info. */
+    protected Map<String, String> mapBuildInfo() {
+        Map<String, String> buildProperties = new HashMap<>();
+        for (IBuildInfo buildInfo : mMasterBuildInfos) {
+            for (Map.Entry<String, String> entry : buildInfo.getBuildAttributes().entrySet()) {
+                String key = entry.getKey();
+                String value = entry.getValue();
+                if (key.startsWith(BUILD_INFO)) {
+                    buildProperties.put(key.substring(CTS_PREFIX.length()), value);
+                }
+            }
+        }
+        return buildProperties;
+    }
+
+    /**
+     * Add build info to results.
+     * @param buildProperties Build info to add.
+     */
+    protected static void addBuildInfoToResult(Map<String, String> buildProperties,
+            IInvocationResult invocationResult) {
+        buildProperties.entrySet().stream().forEach(entry ->
+                invocationResult.addInvocationInfo(entry.getKey(), entry.getValue()));
+    }
+
+    /**
      * Return true if this instance is a shard ResultReporter and should propagate
      * certain events to the master.
      */
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 8d6fbf5..c9d9cbe 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
@@ -27,6 +27,7 @@
 import com.android.compatibility.common.tradefed.result.ChecksumReporterTest;
 import com.android.compatibility.common.tradefed.result.ConsoleReporterTest;
 import com.android.compatibility.common.tradefed.result.MetadataReporterTest;
+import com.android.compatibility.common.tradefed.result.ResultReporterBuildInfoTest;
 import com.android.compatibility.common.tradefed.result.ResultReporterTest;
 import com.android.compatibility.common.tradefed.result.SubPlanHelperTest;
 import com.android.compatibility.common.tradefed.targetprep.PropertyCheckTest;
@@ -78,6 +79,7 @@
     ChecksumReporterTest.class,
     ConsoleReporterTest.class,
     MetadataReporterTest.class,
+    ResultReporterBuildInfoTest.class,
     ResultReporterTest.class,
     SubPlanHelperTest.class,
 
diff --git a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/ResultReporterBuildInfoTest.java b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/ResultReporterBuildInfoTest.java
new file mode 100644
index 0000000..21154ff
--- /dev/null
+++ b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/ResultReporterBuildInfoTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2017 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.result;
+
+
+import com.android.compatibility.common.util.IInvocationResult;
+import com.android.compatibility.common.util.InvocationResult;
+import junit.framework.TestCase;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link ResultReporter}, focused on ability to override build info.
+ */
+public class ResultReporterBuildInfoTest extends TestCase {
+
+    public void testOverrideBuildProperties() {
+        ResultReporterBuildInfoTester tester = new ResultReporterBuildInfoTester();
+        String manufacture = "custom_manufacture";
+        String brand = "google";
+        String product = "gProduct";
+        String device = "gDevice";
+        String version = "gVersion";
+        String buildId = "123";
+        String fingerprint = brand + "/" + product + "/" + device + ":" +
+                version + "/" + buildId + "/userdebug-keys";
+
+        IInvocationResult result = tester.testBuildInfoOverride(fingerprint, manufacture);
+        Map<String, String> invocationInfo = result.getInvocationInfo();
+        assertEquals(invocationInfo.get(ResultReporter.BUILD_ID), buildId);
+        assertEquals(invocationInfo.get(ResultReporter.BUILD_BRAND), brand);
+        assertEquals(invocationInfo.get(ResultReporter.BUILD_DEVICE), device);
+        assertEquals(invocationInfo.get(ResultReporter.BUILD_PRODUCT), product);
+        assertEquals(invocationInfo.get(ResultReporter.BUILD_VERSION_RELEASE), version);
+        assertEquals(invocationInfo.get(ResultReporter.BUILD_FINGERPRINT), fingerprint);
+        assertEquals(invocationInfo.get(ResultReporter.BUILD_MANUFACTURER), manufacture);
+    }
+
+    public static class ResultReporterBuildInfoTester extends ResultReporter {
+
+        public ResultReporterBuildInfoTester() {
+            mResult = new InvocationResult();
+        }
+
+        public IInvocationResult testBuildInfoOverride(String buildFingerprintOverride,
+                String manufactureOverride) {
+            addDeviceBuildInfoToResult(buildFingerprintOverride, manufactureOverride);
+            return mResult;
+        }
+
+        @Override
+        protected Map<String, String> mapBuildInfo() {
+            Map<String, String> buildProperties = new HashMap<>();
+            buildProperties.put(BUILD_ID, BUILD_ID);
+            buildProperties.put(BUILD_BRAND, BUILD_BRAND);
+            buildProperties.put(BUILD_DEVICE, BUILD_DEVICE);
+            buildProperties.put(BUILD_PRODUCT, BUILD_PRODUCT);
+            buildProperties.put(BUILD_VERSION_RELEASE, BUILD_VERSION_RELEASE);
+            buildProperties.put(BUILD_FINGERPRINT, BUILD_FINGERPRINT);
+            buildProperties.put(BUILD_MANUFACTURER, BUILD_MANUFACTURER);
+            return buildProperties;
+        }
+    }
+}