Distinguish between cached and non-cached test runs.

Summary:
Display in the results if a test was read from the cached results or not.

Test Plan:
CachedTestRunReportingIntegrationTest is green.
diff --git a/src/com/facebook/buck/cli/TestCommand.java b/src/com/facebook/buck/cli/TestCommand.java
index 0a72362..2f4a07e 100644
--- a/src/com/facebook/buck/cli/TestCommand.java
+++ b/src/com/facebook/buck/cli/TestCommand.java
@@ -55,6 +55,7 @@
 import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedSet;
@@ -75,6 +76,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 
 import javax.xml.parsers.DocumentBuilder;
@@ -456,7 +458,9 @@
       // because the rule is cached, but its results must still be processed.
       ListenableFuture<TestResults> testResults =
           stepRunner.runStepsAndYieldResult(steps,
-              test.interpretTestResults(executionContext),
+              getCachingStatusTransformingCallable(
+                  isTestRunRequired,
+                  test.interpretTestResults(executionContext)),
               test.getBuildTarget());
       Futures.addCallback(testResults, onTestFinishedCallback);
       results.add(testResults);
@@ -510,6 +514,27 @@
     return failures ? 1 : 0;
   }
 
+  private Callable<TestResults> getCachingStatusTransformingCallable(
+      boolean isTestRunRequired,
+      final Callable<TestResults> originalCallable) {
+    if (isTestRunRequired) {
+      return originalCallable;
+    }
+    return new Callable<TestResults>() {
+      public TestResults call() throws Exception {
+        TestResults originalTestResults = originalCallable.call();
+        ImmutableList<TestCaseSummary> cachedTestResults = FluentIterable
+            .from(originalTestResults.getTestCases())
+            .transform(TestCaseSummary.TO_CACHED_TRANSFORMATION)
+            .toList();
+        return new TestResults(
+            originalTestResults.getBuildTarget(),
+            cachedTestResults,
+            originalTestResults.getContacts());
+      }
+    };
+  }
+
   @VisibleForTesting
   static boolean isTestRunRequiredForTest(
       TestRule test,
diff --git a/src/com/facebook/buck/test/TestCaseSummary.java b/src/com/facebook/buck/test/TestCaseSummary.java
index 37b96b1..83a185a 100644
--- a/src/com/facebook/buck/test/TestCaseSummary.java
+++ b/src/com/facebook/buck/test/TestCaseSummary.java
@@ -18,6 +18,7 @@
 
 import com.facebook.buck.util.Ansi;
 import com.facebook.buck.util.TimeFormat;
+import com.google.common.base.Function;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 
@@ -28,12 +29,28 @@
 @Immutable
 public class TestCaseSummary {
 
+  /**
+   * Transformation to annotate TestCaseSummary marking them as being read from cached results
+   */
+  public final static Function<TestCaseSummary, TestCaseSummary> TO_CACHED_TRANSFORMATION =
+      new Function<TestCaseSummary, TestCaseSummary>() {
+
+        @Override
+        public TestCaseSummary apply(TestCaseSummary summary) {
+          return new TestCaseSummary(summary, /* isCached */ true);
+        }
+      };
+
   private final String testCaseName;
   private final ImmutableList<TestResultSummary> testResults;
   private final boolean isSuccess;
   private final int failureCount;
   private final long totalTime;
+  private final boolean isCached;
 
+  /**
+   * Creates a TestCaseSummary which is assumed to be not read from cached results
+   */
   public TestCaseSummary(String testCaseName, List<TestResultSummary> testResults) {
     this.testCaseName = Preconditions.checkNotNull(testCaseName);
     this.testResults = ImmutableList.copyOf(testResults);
@@ -51,6 +68,18 @@
     this.isSuccess = isSuccess;
     this.failureCount = failureCount;
     this.totalTime = totalTime;
+    this.isCached = false;
+  }
+
+  /** Creates a copy of {@code summary} with the specified value of {@code isCached}. */
+  private TestCaseSummary(TestCaseSummary summary, boolean isCached) {
+    Preconditions.checkNotNull(summary);
+    this.testCaseName = summary.testCaseName;
+    this.testResults = summary.testResults;
+    this.isSuccess = summary.isSuccess;
+    this.failureCount = summary.failureCount;
+    this.totalTime = summary.totalTime;
+    this.isCached = isCached;
   }
 
   public boolean isSuccess() {
@@ -71,7 +100,8 @@
     String status = ansi.asHighlightedStatusText(isSuccess(), isSuccess() ? "PASS" : "FAIL");
     return String.format("%s %s %2d Passed  %2d Failed   %s",
         status,
-        TimeFormat.formatForConsole(totalTime, ansi),
+        !isCached ? TimeFormat.formatForConsole(totalTime, ansi)
+                  : ansi.asHighlightedStatusText(isSuccess(), "CACHED"),
         testResults.size() - failureCount,
         failureCount,
         testCaseName);
diff --git a/test/com/facebook/buck/java/CachedTestRunReportingIntegrationTest.java b/test/com/facebook/buck/java/CachedTestRunReportingIntegrationTest.java
new file mode 100644
index 0000000..5623e5e
--- /dev/null
+++ b/test/com/facebook/buck/java/CachedTestRunReportingIntegrationTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2013-present Facebook, Inc.
+ *
+ * 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.facebook.buck.java;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.facebook.buck.testutil.integration.DebuggableTemporaryFolder;
+import com.facebook.buck.testutil.integration.ProjectWorkspace;
+import com.facebook.buck.testutil.integration.TestDataHelper;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+
+public class CachedTestRunReportingIntegrationTest {
+
+  private final static Charset CHARSET_FOR_TEST = Charsets.UTF_8;
+
+  @Rule
+  public DebuggableTemporaryFolder tmp = new DebuggableTemporaryFolder();
+
+  /**
+   * Test that we correctly report which test runs are cached.
+   */
+  @Test
+  public void testCachedTestRun() throws IOException {
+    tmp.delete();
+    ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
+        this, "cached_test", tmp);
+    workspace.setUp();
+
+    // No caching for this run.
+    assertFalse(
+        "There should not be any caching for the initial test run.",
+        isTestRunCached(workspace, true));
+
+    // Cached Results.
+    assertTrue(
+        "A second test run without any modifications should be cached.",
+        isTestRunCached(workspace, true));
+
+    // Make the test fail.
+    File testFile = workspace.getFile("LameTest.java");
+    String originalJavaCode = Files.toString(testFile, CHARSET_FOR_TEST);
+    String failingJavaCode = originalJavaCode.replace("String str = \"I am not null.\";",
+        "String str = null;");
+    Files.write(failingJavaCode, testFile, CHARSET_FOR_TEST);
+
+    // No caching for this run.
+    assertFalse(
+        "There should not be any caching for this test run.",
+        isTestRunCached(workspace, false));
+
+    // Cached Results.
+    assertTrue(
+        "A second test run without any modifications should be cached.",
+        isTestRunCached(workspace, false));
+  }
+
+  private boolean isTestRunCached(ProjectWorkspace workspace, boolean expectSuccess)
+      throws IOException {
+    ProjectWorkspace.ProcessResult result = workspace.runBuckCommand("test", "//:test");
+    workspace.verify();
+    result.assertExitCode(expectSuccess ? 0 : 1);
+    // Test that Test status is reported
+    assertTrue(result.getStderr().contains("com.example.LameTest"));
+    String status = expectSuccess ? "PASS CACHED" : "FAIL CACHED";
+    return result.getStderr().contains(status);
+  }
+}