Show incremental progress of unit test.

Summary:
By running a unit test, you can see the progress of
the unit test interactively in the message window.
diff --git a/plugin/src/com/facebook/buck/plugin/intellij/BuckPluginComponent.java b/plugin/src/com/facebook/buck/plugin/intellij/BuckPluginComponent.java
index 9b971ff..db40f3c 100644
--- a/plugin/src/com/facebook/buck/plugin/intellij/BuckPluginComponent.java
+++ b/plugin/src/com/facebook/buck/plugin/intellij/BuckPluginComponent.java
@@ -25,6 +25,7 @@
 import com.facebook.buck.plugin.intellij.commands.event.Event;
 import com.facebook.buck.plugin.intellij.commands.event.RuleEnd;
 import com.facebook.buck.plugin.intellij.commands.event.RuleStart;
+import com.facebook.buck.plugin.intellij.commands.event.TestResultsAvailable;
 import com.facebook.buck.plugin.intellij.ui.BuckUI;
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
@@ -219,6 +220,8 @@
         buckUI.getProgressPanel().startRule((RuleStart) event);
       } else if (event instanceof RuleEnd) {
         buckUI.getProgressPanel().endRule((RuleEnd) event);
+      } else if (event instanceof TestResultsAvailable) {
+        buckUI.getProgressPanel().testResult((TestResultsAvailable) event);
       }
     }
   }
diff --git a/plugin/src/com/facebook/buck/plugin/intellij/commands/event/EventFactory.java b/plugin/src/com/facebook/buck/plugin/intellij/commands/event/EventFactory.java
index 90a9160..2de78d0 100644
--- a/plugin/src/com/facebook/buck/plugin/intellij/commands/event/EventFactory.java
+++ b/plugin/src/com/facebook/buck/plugin/intellij/commands/event/EventFactory.java
@@ -25,6 +25,7 @@
 
   public static final String RULE_START = "BuildRuleStarted";
   public static final String RULE_END = "BuildRuleFinished";
+  public static final String TEST_RESULTS_AVAILABLE = "ResultsAvailable";
   private static final Logger LOG = Logger.getInstance(EventFactory.class);
 
   private EventFactory() {}
@@ -35,14 +36,16 @@
     int timestamp = object.get("timestamp").getAsInt();
     String buildId = object.get("buildId").getAsString();
     int threadId = object.get("threadId").getAsInt();
-    if (type.equals(RULE_START)) {
+    if (RULE_START.equals(type)) {
       String name = object.get("buildRule").getAsJsonObject().get("name").getAsString();
       return new RuleStart(timestamp, buildId, threadId, name);
-    } else if (type.equals(RULE_END)) {
+    } else if (RULE_END.equals(type)) {
       String name = object.get("buildRule").getAsJsonObject().get("name").getAsString();
       String status = object.get("status").getAsString();
       String cache = object.get("cacheResult").getAsString();
       return new RuleEnd(timestamp, buildId, threadId, name, status, cache.equals("HIT"));
+    } else if (TEST_RESULTS_AVAILABLE.equals(type)) {
+      return TestResultsAvailable.factory(object, timestamp, buildId, threadId);
     } else {
       LOG.warn("Unhandled message: " + object.toString());
     }
diff --git a/plugin/src/com/facebook/buck/plugin/intellij/commands/event/TestResultsAvailable.java b/plugin/src/com/facebook/buck/plugin/intellij/commands/event/TestResultsAvailable.java
new file mode 100644
index 0000000..450b891
--- /dev/null
+++ b/plugin/src/com/facebook/buck/plugin/intellij/commands/event/TestResultsAvailable.java
@@ -0,0 +1,177 @@
+/*
+ * 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.plugin.intellij.commands.event;
+
+import com.facebook.buck.plugin.intellij.ui.ProgressNode;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import javax.annotation.Nullable;
+
+public class TestResultsAvailable extends Event {
+
+  private final ImmutableList<TestCase> testCases;
+
+  private TestResultsAvailable(int timestamp,
+                               String buildId,
+                               int threadId,
+                               ImmutableList<TestCase> testCases) {
+    super(EventFactory.TEST_RESULTS_AVAILABLE, timestamp, buildId, threadId);
+    this.testCases = Preconditions.checkNotNull(testCases);
+  }
+
+  public static TestResultsAvailable factory(JsonObject object,
+                                             int timestamp,
+                                             String buildId,
+                                             int threadId) {
+    JsonObject resultsObject = object.get("results").getAsJsonObject();
+    JsonArray testCasesObject = resultsObject.get("testCases").getAsJsonArray();
+    ImmutableList.Builder<TestCase> testCasesBuilder = ImmutableList.builder();
+    for (JsonElement element : testCasesObject) {
+      JsonObject testCaseObject = element.getAsJsonObject();
+      TestCase testCase = TestCase.factory(testCaseObject);
+      testCasesBuilder.add(testCase);
+    }
+    ImmutableList<TestCase> testCases = testCasesBuilder.build();
+    return new TestResultsAvailable(timestamp, buildId, threadId, testCases);
+  }
+
+  public ImmutableList<ProgressNode> createTreeNodes() {
+    ImmutableList.Builder<ProgressNode> builder = ImmutableList.builder();
+    for (TestCase testCase : testCases) {
+      builder.add(testCase.createTreeNode(this));
+    }
+    return builder.build();
+  }
+
+  private static class TestCase {
+    private final String testCaseName;
+    private final int totalTime;
+    private final boolean success;
+    private final ImmutableList<TestResult> testResults;
+
+    private TestCase(String testCaseName,
+                     int totalTime,
+                     boolean success,
+                     ImmutableList<TestResult> testResults) {
+      this.testCaseName = Preconditions.checkNotNull(testCaseName);
+      this.totalTime = totalTime;
+      this.success = success;
+      this.testResults = Preconditions.checkNotNull(testResults);
+    }
+
+    public static TestCase factory(JsonObject testCase) {
+      String testCaseName = testCase.get("testCaseName").getAsString();
+      int totalTime = testCase.get("totalTime").getAsInt();
+      boolean success = testCase.get("success").getAsBoolean();
+      JsonArray testResultsObject = testCase.get("testResults").getAsJsonArray();
+      ImmutableList.Builder<TestResult> testResultsBuilder = ImmutableList.builder();
+      for (JsonElement element : testResultsObject) {
+        JsonObject testResultObject = element.getAsJsonObject();
+        TestResult testResult = TestResult.factory(testResultObject);
+        testResultsBuilder.add(testResult);
+      }
+      ImmutableList<TestResult> testResults = testResultsBuilder.build();
+      return new TestCase(testCaseName, totalTime, success, testResults);
+    }
+
+    public ProgressNode createTreeNode(TestResultsAvailable event) {
+      ProgressNode testCaseNode;
+      if (success) {
+        String title = String.format("[%d ms] %s", totalTime, testCaseName);
+        testCaseNode = new ProgressNode(ProgressNode.Type.TEST_CASE_SUCCESS, title, event);
+      } else {
+        testCaseNode = new ProgressNode(ProgressNode.Type.TEST_CASE_FAILURE, testCaseName, event);
+      }
+      for (TestResult testResult : testResults) {
+        ProgressNode testResultNode = testResult.createTreeNode(event);
+        testCaseNode.add(testResultNode);
+      }
+      return testCaseNode;
+    }
+
+    private static class TestResult {
+      private final String testName;
+      private final boolean success;
+      private final int time;
+      @Nullable
+      private final String message;
+      @Nullable
+      @SuppressWarnings("unused")
+      private final String stacktrace;
+      @Nullable
+      @SuppressWarnings("unused")
+      private final String stdOut;
+      @Nullable
+      @SuppressWarnings("unused")
+      private final String stdErr;
+
+      private TestResult(String testName,
+                         boolean success,
+                         int time,
+                         String message,
+                         String stacktrace,
+                         String stdOut,
+                         String stdErr) {
+        this.testName = Preconditions.checkNotNull(testName);
+        this.success = success;
+        this.time = time;
+        this.message = message;
+        this.stacktrace = stacktrace;
+        this.stdOut = stdOut;
+        this.stdErr = stdErr;
+      }
+
+      public static TestResult factory(JsonObject testResult) {
+        String testName = testResult.get("testName").getAsString();
+        boolean success = testResult.get("success").getAsBoolean();
+        int time = testResult.get("time").getAsInt();
+        String message = null;
+        if (!testResult.get("message").isJsonNull()) {
+          message = testResult.get("message").getAsString();
+        }
+        String stacktrace = null;
+        if (!testResult.get("stacktrace").isJsonNull()) {
+          stacktrace = testResult.get("stacktrace").getAsString();
+        }
+        String stdOut = null;
+        if (!testResult.get("stdOut").isJsonNull()) {
+          stdOut = testResult.get("stdOut").getAsString();
+        }
+        String stdErr = null;
+        if (!testResult.get("stdErr").isJsonNull()) {
+          stdErr = testResult.get("stdErr").getAsString();
+        }
+        return new TestResult(testName, success, time, message, stacktrace, stdOut, stdErr);
+      }
+
+      public ProgressNode createTreeNode(TestResultsAvailable event) {
+        if (success) {
+          String title = String.format("[%d ms] %s", time, testName);
+          return new ProgressNode(ProgressNode.Type.TEST_RESULT_SUCCESS, title, event);
+        } else {
+          String title = String.format("%s %s", testName, message);
+          return new ProgressNode(ProgressNode.Type.TEST_RESULT_FAILURE, title, event);
+        }
+      }
+    }
+  }
+}
diff --git a/plugin/src/com/facebook/buck/plugin/intellij/ui/BuckProgressPanel.java b/plugin/src/com/facebook/buck/plugin/intellij/ui/BuckProgressPanel.java
index 4ae5f24..581883c 100644
--- a/plugin/src/com/facebook/buck/plugin/intellij/ui/BuckProgressPanel.java
+++ b/plugin/src/com/facebook/buck/plugin/intellij/ui/BuckProgressPanel.java
@@ -18,7 +18,9 @@
 
 import com.facebook.buck.plugin.intellij.commands.event.RuleEnd;
 import com.facebook.buck.plugin.intellij.commands.event.RuleStart;
+import com.facebook.buck.plugin.intellij.commands.event.TestResultsAvailable;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.intellij.ui.treeStructure.Tree;
 import com.intellij.util.ui.tree.TreeUtil;
@@ -38,6 +40,7 @@
   public static final String TREE_ROOT = "Buck";
   public static final String BUILDING_ROOT = "Building";
   public static final String BUILT_ROOT = "Built";
+  private static final String TEST_ROOT = "Test";
 
   private JPanel panel;
   private JTree tree;
@@ -47,9 +50,11 @@
   private ProgressNode treeRoot;
   private ProgressNode buildingRoot;
   private ProgressNode builtRoot;
+  private ProgressNode testRoot;
   private TreePath rootPath;
   private TreePath buildingPath;
   private TreePath builtPath;
+  private TreePath testPath;
   private List<ProgressNode> items;
 
   public JPanel getPanel() {
@@ -97,6 +102,21 @@
     });
   }
 
+  public void testResult(TestResultsAvailable event) {
+    Preconditions.checkNotNull(event);
+    final ImmutableList<ProgressNode> nodes = event.createTreeNodes();
+    EventQueue.invokeLater(new Runnable() {
+      @Override
+      public void run() {
+        for (ProgressNode node : nodes) {
+          treeModel.insertNodeInto(node, testRoot, testRoot.getChildCount());
+          expand();
+          scrollTo(node);
+        }
+      }
+    });
+  }
+
   private ProgressNode findBuildingNode(RuleEnd current) {
     Preconditions.checkNotNull(current);
     for (ProgressNode item : items) {
@@ -123,15 +143,17 @@
     rootPath = new TreePath(treeRoot);
     treeModel = new DefaultTreeModel(treeRoot);
 
-    builtRoot = new ProgressNode(
-        ProgressNode.Type.DIRECTORY, BUILT_ROOT, null);
+    builtRoot = new ProgressNode(ProgressNode.Type.DIRECTORY, BUILT_ROOT, null);
     builtPath = new TreePath(builtRoot);
     treeModel.insertNodeInto(builtRoot, treeRoot, treeRoot.getChildCount());
 
-    buildingRoot = new ProgressNode(
-        ProgressNode.Type.DIRECTORY, BUILDING_ROOT, null);
+    buildingRoot = new ProgressNode(ProgressNode.Type.DIRECTORY, BUILDING_ROOT, null);
     buildingPath = new TreePath(buildingRoot);
     treeModel.insertNodeInto(buildingRoot, treeRoot, treeRoot.getChildCount());
+
+    testRoot = new ProgressNode(ProgressNode.Type.DIRECTORY, TEST_ROOT, null);
+    testPath = new TreePath(testRoot);
+    treeModel.insertNodeInto(testRoot, treeRoot, treeRoot.getChildCount());
   }
 
   private void expand() {
@@ -144,6 +166,9 @@
     if (!tree.hasBeenExpanded(builtPath)) {
       tree.expandPath(builtPath);
     }
+    if (!tree.hasBeenExpanded(testPath)) {
+      tree.expandPath(testPath);
+    }
   }
 
   private void scrollTo(ProgressNode node) {
diff --git a/plugin/src/com/facebook/buck/plugin/intellij/ui/MessageTreeRenderer.java b/plugin/src/com/facebook/buck/plugin/intellij/ui/MessageTreeRenderer.java
index 9bdcdc8..1eb7ce1 100644
--- a/plugin/src/com/facebook/buck/plugin/intellij/ui/MessageTreeRenderer.java
+++ b/plugin/src/com/facebook/buck/plugin/intellij/ui/MessageTreeRenderer.java
@@ -69,6 +69,22 @@
           prefix = "Error";
           icon = AllIcons.General.Error;
           break;
+        case TEST_CASE_SUCCESS:
+          prefix = "Test";
+          icon = AllIcons.Modules.TestRoot;
+          break;
+        case TEST_CASE_FAILURE:
+          prefix = "Test Failure";
+          icon = AllIcons.General.Error;
+          break;
+        case TEST_RESULT_SUCCESS:
+          prefix = "";
+          icon = AllIcons.Nodes.Advice;
+          break;
+        case TEST_RESULT_FAILURE:
+          prefix = "";
+          icon = AllIcons.General.Error;
+          break;
         default:
           icon = AllIcons.General.Error;
           prefix = "";
diff --git a/plugin/src/com/facebook/buck/plugin/intellij/ui/ProgressNode.java b/plugin/src/com/facebook/buck/plugin/intellij/ui/ProgressNode.java
index 0de1f46..734c36b7 100644
--- a/plugin/src/com/facebook/buck/plugin/intellij/ui/ProgressNode.java
+++ b/plugin/src/com/facebook/buck/plugin/intellij/ui/ProgressNode.java
@@ -29,7 +29,11 @@
     BUILDING,
     BUILT,
     BUILT_CACHED,
-    BUILD_ERROR
+    BUILD_ERROR,
+    TEST_CASE_SUCCESS,
+    TEST_CASE_FAILURE,
+    TEST_RESULT_SUCCESS,
+    TEST_RESULT_FAILURE
   }
 
   private String name;