Add TestRunEnd metrics to getLastCommandResult operation.

Change-Id: I6e1bb8654f8fe677b33b8c4d56021de2a29e512b
diff --git a/Android.mk b/Android.mk
index 57fbf1e..e667cdb 100644
--- a/Android.mk
+++ b/Android.mk
@@ -26,7 +26,7 @@
 LOCAL_MODULE := tradefed
 
 LOCAL_MODULE_TAGS := optional
-LOCAL_STATIC_JAVA_LIBRARIES := junit kxml2-2.3.0 guavalib jline-1.0
+LOCAL_STATIC_JAVA_LIBRARIES := junit kxml2-2.3.0 jline-1.0
 # emmalib is only a runtime dependency if generating code coverage reporters,
 # not a compile time dependency
 LOCAL_JAVA_LIBRARIES := ddmlib-prebuilt emmalib tools-common-prebuilt tf-remote-client
diff --git a/remote/.classpath b/remote/.classpath
index 72f62ef..fc4759a 100644
--- a/remote/.classpath
+++ b/remote/.classpath
@@ -3,6 +3,7 @@
 	<classpathentry kind="src" path="src"/>
 	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
 	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/json/json-prebuilt.jar"/>
+	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/guavalib_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/guava/guava/src"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/remote/Android.mk b/remote/Android.mk
index a0fbc14..0793f33 100644
--- a/remote/Android.mk
+++ b/remote/Android.mk
@@ -26,7 +26,7 @@
 LOCAL_MODULE_TAGS := optional
 # only depend on ddmlib for the Log class
 LOCAL_JAVA_LIBRARIES := ddmlib-prebuilt
-LOCAL_STATIC_JAVA_LIBRARIES := json-prebuilt
+LOCAL_STATIC_JAVA_LIBRARIES := json-prebuilt guavalib
 
 include $(BUILD_HOST_JAVA_LIBRARY)
 
diff --git a/remote/src/com/android/tradefed/command/remote/CommandResult.java b/remote/src/com/android/tradefed/command/remote/CommandResult.java
index 4cfd87c..78939f2 100644
--- a/remote/src/com/android/tradefed/command/remote/CommandResult.java
+++ b/remote/src/com/android/tradefed/command/remote/CommandResult.java
@@ -17,6 +17,8 @@
 
 import com.android.tradefed.device.FreeDeviceState;
 
+import java.util.Map;
+
 /**
  * Encapsulates 'last command execution' data sent over the wire
  */
@@ -33,15 +35,18 @@
     private final Status mStatus;
     private final String mError;
     private final FreeDeviceState mState;
+    private final Map<String, String> mRunMetrics;
 
-    CommandResult(Status status, String errorDetails, FreeDeviceState state) {
+    CommandResult(Status status, String errorDetails, FreeDeviceState state,
+            Map<String, String> runMetrics) {
         mStatus = status;
         mError = errorDetails;
         mState = state;
+        mRunMetrics = runMetrics;
     }
 
     public CommandResult(Status status) {
-        this(status, null, null);
+        this(status, null, null, null);
     }
 
     Status getStatus() {
@@ -55,4 +60,12 @@
     FreeDeviceState getFreeDeviceState() {
         return mState;
     }
+
+    /*
+     * Although commands can have multiple runs, we only return one set of metrics and replace any
+     * currently stored metrics with the same key.
+     */
+    Map<String, String> getRunMetrics() {
+        return mRunMetrics;
+    }
 }
diff --git a/remote/src/com/android/tradefed/command/remote/GetLastCommandResultOp.java b/remote/src/com/android/tradefed/command/remote/GetLastCommandResultOp.java
index 09b2ab1..3fcc010 100644
--- a/remote/src/com/android/tradefed/command/remote/GetLastCommandResultOp.java
+++ b/remote/src/com/android/tradefed/command/remote/GetLastCommandResultOp.java
@@ -15,12 +15,17 @@
  */
 package com.android.tradefed.command.remote;
 
+import com.google.common.collect.ImmutableMap;
+
 import com.android.tradefed.command.remote.CommandResult.Status;
 import com.android.tradefed.device.FreeDeviceState;
 
+import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import java.util.Map;
+
 /**
  * Remote operation for getting the result of the last command executed
  */
@@ -31,6 +36,9 @@
     private static final String FREE_DEVICE_STATE = "free_device_state";
     private static final String STATUS = "status";
     private static final String INVOCATION_ERROR = "invocation_error";
+    private static final String RUN_METRICS = "run_metrics";
+    private static final String RUN_METRICS_KEY = "key";
+    private static final String RUN_METRICS_VALUE = "value";
 
     private final String mSerial;
 
@@ -88,7 +96,21 @@
                 throw new JSONException(String.format("unrecognized state '%s'", freeDeviceString));
             }
         }
-        return new CommandResult(status, errorDetails, state);
+
+        Map<String, String> runMetricsMap = null;
+        JSONArray jsonRunMetricsArray = json.optJSONArray(RUN_METRICS);
+        if (jsonRunMetricsArray != null && jsonRunMetricsArray.length() > 0) {
+            ImmutableMap.Builder<String, String> mapBuilder =
+                    new ImmutableMap.Builder<String, String>();
+            for (int i = 0; i < jsonRunMetricsArray.length(); i++) {
+                JSONObject runMetricJson = jsonRunMetricsArray.getJSONObject(i);
+                final String key = runMetricJson.getString(RUN_METRICS_KEY);
+                final String value = runMetricJson.getString(RUN_METRICS_VALUE);
+                mapBuilder.put(key, value);
+            }
+            runMetricsMap = mapBuilder.build();
+        }
+        return new CommandResult(status, errorDetails, state, runMetricsMap);
     }
 
     /**
@@ -103,6 +125,17 @@
         if (commandResult.getFreeDeviceState() != null) {
             json.put(FREE_DEVICE_STATE, commandResult.getFreeDeviceState().name());
         }
+        Map<String, String> runMetrics = commandResult.getRunMetrics();
+        if (runMetrics != null && runMetrics.size() > 0) {
+            JSONArray jsonRunMetricsArray = new JSONArray();
+            for (Map.Entry<String, String> entry : runMetrics.entrySet()) {
+                JSONObject keyValuePairJson = new JSONObject();
+                keyValuePairJson.put(RUN_METRICS_KEY, entry.getKey());
+                keyValuePairJson.put(RUN_METRICS_VALUE, entry.getValue());
+                jsonRunMetricsArray.put(keyValuePairJson);
+            }
+            json.put(RUN_METRICS, jsonRunMetricsArray);
+        }
     }
 
     public String getDeviceSerial() {
diff --git a/remote/src/com/android/tradefed/command/remote/ICommandResultHandler.java b/remote/src/com/android/tradefed/command/remote/ICommandResultHandler.java
index 6762a57..68053a3 100644
--- a/remote/src/com/android/tradefed/command/remote/ICommandResultHandler.java
+++ b/remote/src/com/android/tradefed/command/remote/ICommandResultHandler.java
@@ -17,12 +17,17 @@
 
 import com.android.tradefed.device.FreeDeviceState;
 
+import java.util.Map;
+
 /**
- * Callback for handling the result of a {@link GetLastCommandResultOp}
+ * Callback for handling the result of a {@link GetLastCommandResultOp}. Although commands can have
+ * multiple runs, we only return one set of metrics and replace any currently stored metrics with
+ * the same key.
  */
 public interface ICommandResultHandler {
-    public void success();
-    public void failure(String errorDetails, FreeDeviceState deviceState);
+    public void success(Map<String, String> runMetrics);
+    public void failure(String errorDetails, FreeDeviceState deviceState,
+            Map<String, String> runMetrics);
     public void stillRunning();
     public void notAllocated();
     public void noActiveCommand();
diff --git a/remote/src/com/android/tradefed/command/remote/RemoteClient.java b/remote/src/com/android/tradefed/command/remote/RemoteClient.java
index 86a23b8..2d06cf9 100644
--- a/remote/src/com/android/tradefed/command/remote/RemoteClient.java
+++ b/remote/src/com/android/tradefed/command/remote/RemoteClient.java
@@ -200,10 +200,11 @@
                 handler.stillRunning();
                 break;
             case INVOCATION_ERROR:
-                handler.failure(r.getInvocationErrorDetails(), r.getFreeDeviceState());
+                handler.failure(r.getInvocationErrorDetails(), r.getFreeDeviceState(),
+                        r.getRunMetrics());
                 break;
             case INVOCATION_SUCCESS:
-                handler.success();
+                handler.success(r.getRunMetrics());
                 break;
             case NO_ACTIVE_COMMAND:
                 handler.noActiveCommand();
diff --git a/remote/src/com/android/tradefed/command/remote/RemoteOperation.java b/remote/src/com/android/tradefed/command/remote/RemoteOperation.java
index 11e4906..e0fabfe 100644
--- a/remote/src/com/android/tradefed/command/remote/RemoteOperation.java
+++ b/remote/src/com/android/tradefed/command/remote/RemoteOperation.java
@@ -28,7 +28,7 @@
     /** represents json key for error message */
     static final String ERROR = "error";
 
-    static final int CURRENT_PROTOCOL_VERSION = 4;
+    static final int CURRENT_PROTOCOL_VERSION = 5;
 
     /**
      * Represents all types of remote operations that can be performed
diff --git a/src/com/android/tradefed/command/remote/ExecCommandTracker.java b/src/com/android/tradefed/command/remote/ExecCommandTracker.java
index 7139854..20c524a 100644
--- a/src/com/android/tradefed/command/remote/ExecCommandTracker.java
+++ b/src/com/android/tradefed/command/remote/ExecCommandTracker.java
@@ -20,8 +20,12 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.result.StubTestInvocationListener;
 
+import com.google.common.collect.ImmutableMap;
+
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
+import java.util.HashMap;
+import java.util.Map;
 
 class ExecCommandTracker extends StubTestInvocationListener implements
         IScheduledInvocationListener {
@@ -29,6 +33,7 @@
     private CommandResult.Status mStatus = CommandResult.Status.EXECUTING;
     private String mErrorDetails = null;
     private FreeDeviceState mState = null;
+    Map<String, String> mRunMetrics = new HashMap<String, String>();
 
     @Override
     public void invocationFailed(Throwable cause) {
@@ -49,11 +54,18 @@
         }
     }
 
+    @Override
+    public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
+        mRunMetrics.putAll(runMetrics);
+    }
+
     /**
      * Returns the current state as a {@link CommandResult}.
      * @return
      */
     CommandResult getCommandResult() {
-        return new CommandResult(mStatus, mErrorDetails, mState);
+        return new CommandResult(mStatus, mErrorDetails, mState,
+                new ImmutableMap.Builder<String, String>()
+                    .putAll(mRunMetrics).build());
     }
 }
diff --git a/tests/src/com/android/tradefed/command/remote/RemoteManagerTest.java b/tests/src/com/android/tradefed/command/remote/RemoteManagerTest.java
index 15a1d6f..62db318 100644
--- a/tests/src/com/android/tradefed/command/remote/RemoteManagerTest.java
+++ b/tests/src/com/android/tradefed/command/remote/RemoteManagerTest.java
@@ -31,6 +31,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Unit tests for {@link RemoteManager}.
@@ -404,9 +405,11 @@
     /**
      * Test {@link GetLastCommandResultOp} result when commmand fails due to a not available device.
      */
+    @SuppressWarnings("unchecked")
     public void testGetLastCommandResult_notAvail() throws Exception {
         ICommandResultHandler mockHandler = EasyMock.createStrictMock(ICommandResultHandler.class);
-        mockHandler.failure((String)EasyMock.anyObject(), EasyMock.eq(FreeDeviceState.UNAVAILABLE));
+        mockHandler.failure((String)EasyMock.anyObject(), EasyMock.eq(FreeDeviceState.UNAVAILABLE),
+                (Map<String, String>)EasyMock.anyObject());
         ITestDevice device = EasyMock.createMock(ITestDevice.class);
         EasyMock.expect(device.getSerialNumber()).andStubReturn("serial");
         EasyMock.expect(mMockDeviceManager.forceAllocateDevice("serial")).andReturn(device);