Example #1
0
/**
 * This is the controller for the Apex Test Runner view. It's responsible for running tests, getting
 * results, and updating the UI with the test results. The actual view is generated by
 * RunTestViewComposite.java.
 *
 * @author jwidjaja
 */
public class RunTestView extends BaseViewPart {
  private static final Logger logger = Logger.getLogger(RunTestView.class);

  private static RunTestView INSTANCE = null;
  private final ReentrantLock lock = new ReentrantLock();

  // The name that is shown on the view tab
  public static final String VIEW_NAME = "Apex Test Runner";

  // Keys used to store data in a TreeItem
  public static final String TREEDATA_TEST_RESULT = "ApexTestResult";
  public static final String TREEDATA_CODE_LOCATION = "ApexCodeLocation";
  public static final String TREEDATA_APEX_LOG = "ApexLog";
  public static final String TREEDATA_APEX_LOG_USER_DEBUG = "ApexLogUserDebug";
  public static final String TREEDATA_APEX_LOG_BODY = "ApexLogBody";

  private RunTestViewComposite runTestComposite = null;
  private IProject project = null;
  private ForceProject forceProject = null;
  private LogInfo[] logInfos = null;
  private HTTPConnection toolingRESTConnection = null;
  private ToolingStubExt toolingStubExt = null;
  private ISelectionListener fPostSelectionListener = null;

  private final Image FAILURE_ICON = ForceImages.get(ForceImages.IMAGE_FAILURE);
  private final int FAILURE_COLOR = SWT.COLOR_RED;
  private final Image WARNING_ICON = ForceImages.get(ForceImages.IMAGE_WARNING);
  private final int WARNING_COLOR = SWT.COLOR_DARK_YELLOW;
  private final Image PASS_ICON = ForceImages.get(ForceImages.IMAGE_CONFIRM);
  private final int PASS_COLOR = SWT.COLOR_DARK_GREEN;

  private final String TOOLING_ENDPOINT = "/services/data/";

  private final String QUERY_USER_ID = "SELECT Id, Username FROM User WHERE Username = '******'";
  private final String QUERY_TESTRESULT_COUNT =
      "SELECT COUNT(Id) FROM ApexTestResult WHERE AsyncApexJobId = '%s'";
  private final String QUERY_TESTRESULT =
      "SELECT ApexClassId, ApexLogId, AsyncApexJobId, Message, "
          + "MethodName, Outcome, QueueItemId, StackTrace, TestTimestamp "
          + "FROM ApexTestResult WHERE AsyncApexJobId = '%s'";
  private final String QUERY_APEX_LOG =
      "SELECT Id, Application, DurationMilliseconds, Location, LogLength, LogUserId, Operation, Request, StartTime, Status FROM ApexLog WHERE Id = '%s'";
  private final String QUERY_APEX_TEST_QUEUE_ITEM =
      "SELECT Id, Status FROM ApexTestQueueItem WHERE ParentJobId = '%s'";

  private final int POLL_FAST = 5000;
  private final int POLL_MED = 10000;
  private final int POLL_SLOW = 15000;

  public RunTestView() {
    super();
    setSelectionListener();

    INSTANCE = this;
  }

  public static RunTestView getInstance() {
    if (Utils.isEmpty(INSTANCE)) {
      // We use Display.syncExec because getting a view has to be done
      // on a UI thread.
      Display display = PlatformUI.getWorkbench().getDisplay();
      if (Utils.isEmpty(display)) return INSTANCE;

      display.syncExec(
          new Runnable() {
            @Override
            public void run() {
              try {
                INSTANCE =
                    (RunTestView)
                        PlatformUI.getWorkbench()
                            .getActiveWorkbenchWindow()
                            .getActivePage()
                            .showView(UIConstants.RUN_TEST_VIEW_ID);
              } catch (PartInitException e) {
              }
            }
          });
    }

    return INSTANCE;
  }

  /**
   * Is there a test run in progress? If yes, this method returns false. If no, this method returns
   * true.
   */
  public boolean canRun() {
    return (lock != null && !lock.isLocked());
  }

  /**
   * Run the tests, get the results, and update the UI.
   *
   * @param project
   * @param testResources
   * @param testsInJson
   * @param totalTestMethods
   * @param monitor
   */
  public void runTests(
      final IProject project,
      Map<String, IResource> testResources,
      String testsInJson,
      int totalTestMethods,
      IProgressMonitor monitor) {
    lock.lock();

    try {
      // Prepare the UI and get the log infos
      Display display = PlatformUI.getWorkbench().getDisplay();
      display.syncExec(
          new Runnable() {
            @Override
            public void run() {
              setProject(project);
              runTestComposite.clearAll();
              logInfos = runTestComposite.getLogInfoAndType();
            }
          });

      forceProject = materializeForceProject(project);
      // We need the user ID to insert a trace flag
      String userId = getUserId(forceProject.getUserName());
      // Try to insert a trace flag for apex logs. If this fails, the test run will still continue
      String traceFlagId = insertTraceFlag(logInfos, userId);

      // If user wants to cancel the launch, delete the trace flag and stop
      if (monitor.isCanceled()) {
        deleteTraceFlag(traceFlagId);
        return;
      }

      // Submit the tests for execution
      String testRunId = enqueueTests(testsInJson);

      // Poll for test results
      List<ApexTestResult> testResults = getTestResults(testRunId, totalTestMethods, monitor);

      // Whether or not user aborted, we want to delete the trace flag
      deleteTraceFlag(traceFlagId);

      // Whether or not user aborted, we want to show whatever test results we got back
      processTestResults(project, testResources, testResults);
    } finally {
      lock.unlock();
    }
  }

  /**
   * Get a ForceProject from an IProject.
   *
   * @param project
   * @return ForceProject
   */
  public ForceProject materializeForceProject(IProject project) {
    if (Utils.isEmpty(project) || !project.exists()) return null;

    ForceProject forceProject =
        ContainerDelegate.getInstance()
            .getServiceLocator()
            .getProjectService()
            .getForceProject(project);
    return forceProject;
  }

  /**
   * Queries the user ID based on the given user name.
   *
   * @param userName
   * @return User ID if valid. Null otherwise.
   */
  public String getUserId(String userName) {
    String userId = null;

    if (Utils.isEmpty(forceProject) || Utils.isEmpty(userName)) {
      return userId;
    }

    try {
      initializeConnection(forceProject);

      QueryResult qr = toolingStubExt.query(String.format(QUERY_USER_ID, userName));
      if (qr != null && qr.getSize() > 0) {
        return qr.getRecords()[0].getId();
      }
    } catch (ForceConnectionException | ForceRemoteException e) {
      logger.error("Failed to connect to Tooling API", e);
    }

    return userId;
  }

  /**
   * Inserts a TraceFlag. The userId is used for the TraceFlag's entity ID and scope ID.
   *
   * @param logInfos
   * @param userId
   * @return ID of the TraceFlag
   */
  public String insertTraceFlag(LogInfo[] logInfos, String userId) {
    String traceFlagId = null;

    if (Utils.isEmpty(forceProject) || Utils.isEmpty(logInfos) || Utils.isEmpty(userId)) {
      return traceFlagId;
    }

    try {
      initializeConnection(forceProject);

      TraceFlag tf = new TraceFlag();
      tf.setTracedEntityId(userId);

      for (LogInfo logInfo : logInfos) {
        // Translate Metadata's LogInfo into Tooling's ApexLogLevel and LogCategory
        LogCategory logCategory = logInfo.getCategory();
        LogCategoryLevel metadataLogLevel = logInfo.getLevel();

        ApexLogLevel toolingLogLevel = translateLogLevel(metadataLogLevel);

        if (logCategory.equals(LogCategory.Apex_code)) {
          tf.setApexCode(toolingLogLevel);
        } else if (logCategory.equals(LogCategory.Apex_profiling)) {
          tf.setApexProfiling(toolingLogLevel);
        } else if (logCategory.equals(LogCategory.Callout)) {
          tf.setCallout(toolingLogLevel);
        } else if (logCategory.equals(LogCategory.Db)) {
          tf.setDatabase(toolingLogLevel);
        } else if (logCategory.equals(LogCategory.System)) {
          tf.setSystem(toolingLogLevel);
        } else if (logCategory.equals(LogCategory.Validation)) {
          tf.setValidation(toolingLogLevel);
        } else if (logCategory.equals(LogCategory.Visualforce)) {
          tf.setVisualforce(toolingLogLevel);
        } else if (logCategory.equals(LogCategory.Workflow)) {
          tf.setWorkflow(toolingLogLevel);
        }
      }

      SaveResult[] sr = toolingStubExt.create(new SObject[] {tf});
      if (sr != null && sr.length > 0) {
        traceFlagId = sr[0].getId();
        if (sr[0].isSuccess()) {
          logger.debug(String.format("Created TraceFlag %s", traceFlagId));
        } else {
          logger.warn(sr[0].getErrors().toString());
        }
      }
    } catch (ForceConnectionException | ForceRemoteException e) {
      logger.error("Failed to connect to Tooling API", e);
    }

    return traceFlagId;
  }

  /**
   * Delete a TraceFlag.
   *
   * @param traceFlagId
   */
  public void deleteTraceFlag(String traceFlagId) {
    if (Utils.isEmpty(forceProject) || Utils.isEmpty(traceFlagId)) {
      return;
    }

    try {
      initializeConnection(forceProject);

      DeleteResult[] dr = toolingStubExt.delete(new String[] {traceFlagId});
      if (dr != null && dr.length > 0) {
        boolean deleteSuccess = dr[0].isSuccess();
        if (deleteSuccess) {
          logger.debug(String.format("Deleted TraceFlag %s", traceFlagId));
        } else {
          logger.warn(String.format("Failed to delete TraceFlag %s", traceFlagId));
        }
      }
    } catch (ForceConnectionException | ForceRemoteException e) {
      logger.error("Failed to connect to Tooling API", e);
    }
  }

  /**
   * Translate Metadata's LogCategoryLevel to Tooling's ApexLogLevel.
   *
   * @param metadataLogLevel
   * @return The translated log level
   */
  private ApexLogLevel translateLogLevel(LogCategoryLevel metadataLogLevel) {
    if (metadataLogLevel.equals(LogCategoryLevel.Debug)) {
      return ApexLogLevel.DEBUG;
    } else if (metadataLogLevel.equals(LogCategoryLevel.Error)) {
      return ApexLogLevel.ERROR;
    } else if (metadataLogLevel.equals(LogCategoryLevel.Fine)) {
      return ApexLogLevel.FINE;
    } else if (metadataLogLevel.equals(LogCategoryLevel.Finer)) {
      return ApexLogLevel.FINER;
    } else if (metadataLogLevel.equals(LogCategoryLevel.Finest)) {
      return ApexLogLevel.FINEST;
    } else if (metadataLogLevel.equals(LogCategoryLevel.Info)) {
      return ApexLogLevel.INFO;
    } else if (metadataLogLevel.equals(LogCategoryLevel.Warn)) {
      return ApexLogLevel.WARN;
    } else {
      return ApexLogLevel.NONE;
    }
  }

  /**
   * Enqueue a tests array to Tooling's runTestsAsynchronous.
   *
   * @param testsInJson
   * @return The test run ID if valid. Null otherwise.
   */
  public String enqueueTests(String testsInJson) {
    String response = null;

    if (Utils.isEmpty(forceProject)) {
      return response;
    }

    try {
      initializeConnection(forceProject);

      PromiseableJob<String> job =
          new RunTestsCommand(
              new HTTPAdapter<>(
                  String.class, new RunTestsTransport(toolingRESTConnection), HTTPMethod.POST),
              testsInJson);
      job.schedule();

      try {
        job.join();
        response = job.getAnswer();
      } catch (InterruptedException e) {
        logger.error("Failed to enqueue test run", e);
      }

    } catch (ForceConnectionException | ForceRemoteException e) {
      logger.error("Failed to connect to Tooling API", e);
    }

    return response;
  }

  /**
   * Retrieve test results for the given test run ID.
   *
   * @param testRunId
   * @return A list of ApexTestResult, if any.
   */
  public List<ApexTestResult> getTestResults(
      final String testRunId, final int totalTestMethods, IProgressMonitor monitor) {
    List<ApexTestResult> testResults = Lists.newArrayList();
    if (Utils.isEmpty(forceProject) || Utils.isEmpty(testRunId)) return testResults;

    try {
      initializeConnection(forceProject);

      // Get remaining daily API requests
      Limit dailyApiRequests = getApiLimit(forceProject, LimitsCommand.Type.DailyApiRequests);
      if (dailyApiRequests == null) {
        return testResults;
      }

      float apiRequestsRemaining =
          (dailyApiRequests.getRemaining() * 100.0f) / dailyApiRequests.getMax();
      if (apiRequestsRemaining <= 0) {
        return testResults;
      }

      // Poll for remaining test cases to be executed
      int totalTestDone = 0;
      QueryResult qr = null;
      // No timeout here because we don't know how long a test run can be.
      // If user wants to exit, then they can cancel the launch config.
      while (totalTestDone < totalTestMethods) {
        // Query for number of finished tests in specified test run
        qr = toolingStubExt.query(String.format(QUERY_TESTRESULT_COUNT, testRunId));

        // Update finished test counter
        if (qr.getSize() == 1) {
          SObject sObj = qr.getRecords()[0];
          if (sObj instanceof AggregateResult) {
            AggregateResult aggRes = (AggregateResult) sObj;
            Object expr0 = aggRes.getField("expr0");
            totalTestDone = (int) expr0;
            updateProgress(0, totalTestMethods, totalTestDone);
          }
        }

        // User wants to abort so we'll tell the server to abort the test run
        // and stop polling for test results. There may be some finished test results
        // so try to query those and update UI if necessary.
        if (monitor.isCanceled()) {
          abortTestRun(testRunId);
          break;
        }

        // Wait according to the interval
        int wait = getPollInterval(totalTestMethods - totalTestDone, apiRequestsRemaining);
        Thread.sleep(wait);
      }

      // Get all test results in the specified test run
      qr = toolingStubExt.query(String.format(QUERY_TESTRESULT, testRunId));
      if (qr != null && qr.getSize() > 0) {
        updateProgress(0, totalTestMethods, qr.getSize());
        for (SObject sObj : qr.getRecords()) {
          ApexTestResult testResult = (ApexTestResult) sObj;
          testResults.add(testResult);
        }
      }
    } catch (ForceConnectionException | ForceRemoteException e) {
      logger.error("Failed to connect to Tooling API", e);
    } catch (InterruptedException e) {
      logger.error("Getting test results was interrupted", e);
    } catch (Exception e) {
      logger.error("Something unexpected with getting Apex Test results", e);
    }

    return testResults;
  }

  /**
   * Get a specific API Limit
   *
   * @param type
   * @return Limit
   * @see Limit.java
   */
  public Limit getApiLimit(ForceProject forceProject, LimitsCommand.Type type) {
    try {
      initializeConnection(forceProject);

      PromiseableJob<Map<String, Limit>> job =
          new LimitsCommand(
              new HTTPAdapter<>(
                  String.class, new LimitsTransport(toolingRESTConnection), HTTPMethod.GET));
      job.schedule();

      try {
        job.join();
        Map<String, Limit> limits = job.getAnswer();
        if (limits != null && limits.size() > 0) {
          return limits.get(type.toString());
        }
      } catch (InterruptedException e) {
        logger.error("Failed to enqueue test run", e);
      }
    } catch (ForceConnectionException | ForceRemoteException e) {
      logger.error("Failed to connect to Tooling API", e);
    }

    return null;
  }

  /**
   * Get the appropriate poll interval depending on the number of tests remaining and the number of
   * API requests remaining. The higher the number of tests remaining, the slower we should poll.
   * The higher the number of remaining API requests, the faster we should poll.
   *
   * @param totalTestRemaining
   * @param apiRequestsRemaining
   * @return A poll interval
   */
  public int getPollInterval(int totalTestRemaining, float apiRequestsRemaining) {
    int intervalA = POLL_SLOW, intervalB = POLL_SLOW;

    if (totalTestRemaining <= 10) {
      intervalA = POLL_FAST;
    } else if (totalTestRemaining <= 50) {
      intervalA = POLL_MED;
    } else {
      intervalA = POLL_SLOW;
    }

    if (apiRequestsRemaining <= 25f) {
      intervalB = POLL_SLOW;
    } else if (apiRequestsRemaining <= 50f) {
      intervalB = POLL_MED;
    } else {
      intervalB = POLL_FAST;
    }

    return (intervalA + intervalB) / 2;
  }

  /**
   * Update the progress bar to show user the number of tests finished.
   *
   * @param min
   * @param max
   * @param cur
   */
  public void updateProgress(final int min, final int max, final int cur) {
    Display display = PlatformUI.getWorkbench().getDisplay();
    display.syncExec(
        new Runnable() {
          @Override
          public void run() {
            if (Utils.isNotEmpty(runTestComposite)) {
              runTestComposite.setProgress(min, max, cur);
            }
          }
        });
  }

  /**
   * Abort all ApexTestQueueItem with the same test run ID.
   *
   * @param testRunId
   */
  public void abortTestRun(String testRunId) {
    try {
      initializeConnection(forceProject);

      // Get all ApexTestQueueItem in the test run
      QueryResult qr = toolingStubExt.query(String.format(QUERY_APEX_TEST_QUEUE_ITEM, testRunId));
      if (Utils.isEmpty(qr) || qr.getSize() == 0) return;

      List<ApexTestQueueItem> abortedList = Lists.newArrayList();
      for (SObject sObj : qr.getRecords()) {
        ApexTestQueueItem atqi = (ApexTestQueueItem) sObj;
        // If the queue item is not done yet, abort them
        if (!atqi.getStatus().equals(AsyncApexJobStatus.Completed)
            && !atqi.getStatus().equals(AsyncApexJobStatus.Failed)) {
          atqi.setStatus(AsyncApexJobStatus.Aborted);
          abortedList.add(atqi);
        }
      }

      if (!abortedList.isEmpty()) {
        ApexTestQueueItem[] abortedArray = new ApexTestQueueItem[abortedList.size()];
        for (int i = 0; i < abortedList.size(); i++) {
          abortedArray[i] = abortedList.get(i);
        }

        toolingStubExt.update(abortedArray);
      }
    } catch (ForceConnectionException | ForceRemoteException e) {
      logger.error("Failed to connect to Tooling API", e);
    }
  }

  /**
   * Update the UI with the test results.
   *
   * @param project
   * @param testResources
   * @param testResults
   * @param delegate
   */
  public void processTestResults(
      final IProject project,
      final Map<String, IResource> testResources,
      final List<ApexTestResult> testResults) {
    Display display = PlatformUI.getWorkbench().getDisplay();

    display.asyncExec(
        new Runnable() {
          @Override
          public void run() {
            if (Utils.isEmpty(project)
                || Utils.isEmpty(testResources)
                || testResults == null
                || testResults.isEmpty()) {
              return;
            }

            // Map of tree items whose key is apex class id and the value is the tree item
            Map<String, TreeItem> testClassNodes = new HashMap<String, TreeItem>();

            FontRegistry registry = new FontRegistry();
            Font boldFont =
                registry.getBold(Display.getCurrent().getSystemFont().getFontData()[0].getName());

            // Reset tree
            Tree resultsTree = runTestComposite.getTree();
            resultsTree.removeAll();

            // Add each test result to the tree
            for (ApexTestResult testResult : testResults) {
              // Create or find the tree node for the test class
              String classId = testResult.getApexClassId();
              String className = null;
              if (!testClassNodes.containsKey(classId)) {
                TreeItem newClassNode = createTestClassTreeItem(resultsTree, boldFont);
                // Test result only has test class ID. Find the test class name mapped to that ID to
                // display in UI
                className =
                    testResources.containsKey(classId)
                        ? testResources.get(classId).getName()
                        : classId;
                newClassNode.setText(className);
                // Save the associated file in the tree item
                IFile testFile = getFileFromId(testResources, classId);
                if (Utils.isNotEmpty(testFile)) {
                  // For test classes, always point to the first line of the file
                  ApexCodeLocation location = new ApexCodeLocation(testFile, 1, 1);
                  newClassNode.setData(TREEDATA_CODE_LOCATION, location);
                }

                testClassNodes.put(classId, newClassNode);
              }

              // Add the a test method tree node to the test class tree node
              TreeItem classNode = testClassNodes.get(classId);
              className = classNode.getText();

              // Create a tree item for the test method and save the test result
              TreeItem newTestMethodNode =
                  createTestMethodTreeItem(classNode, testResult, className);
              // Set the color and icon of test method tree node based on test outcome
              setColorAndIconForNode(newTestMethodNode, testResult.getOutcome());
              // Update the color & icon of class tree node only if the test method
              // outcome is worse than what the class tree node indicates
              setColorAndIconForTheWorse(classNode, testResult.getOutcome());
            }

            // Expand the test classes that did not pass
            expandProblematicTestClasses(resultsTree);
          }
        });
  }

  /**
   * Create a default TreeItem for a test class.
   *
   * @param parent
   * @param font
   * @return TreeItem for test class
   */
  private TreeItem createTestClassTreeItem(Tree parent, Font font) {
    TreeItem newClassNode = new TreeItem(parent, SWT.NONE);
    newClassNode.setFont(font);
    newClassNode.setExpanded(false);
    // Mark this test class as pass until we find a test method within it that says otherwise
    setColorAndIconForNode(newClassNode, ApexTestOutcome.Pass);
    return newClassNode;
  }

  /**
   * Set color and icon for a test method's TreeItem.
   *
   * @param node
   * @param outcome
   */
  private void setColorAndIconForNode(TreeItem node, ApexTestOutcome outcome) {
    if (Utils.isEmpty(node) || Utils.isEmpty(outcome)) return;

    Display display = node.getDisplay();
    if (outcome.equals(ApexTestOutcome.Pass)) {
      node.setForeground(display.getSystemColor(PASS_COLOR));
      node.setImage(PASS_ICON);
    } else if (outcome.equals(ApexTestOutcome.Skip)) {
      node.setForeground(display.getSystemColor(WARNING_COLOR));
      node.setImage(WARNING_ICON);
    } else {
      node.setForeground(display.getSystemColor(FAILURE_COLOR));
      node.setImage(FAILURE_ICON);
    }
  }

  /**
   * Update the color & icon of a TreeItem only if the given outcome is worse than what the TreeItem
   * already indicates.
   *
   * @param node
   * @param outcome
   */
  private void setColorAndIconForTheWorse(TreeItem node, ApexTestOutcome outcome) {
    if (Utils.isEmpty(node) || Utils.isEmpty(outcome)) return;

    Image curImage = node.getImage();
    boolean worseThanPass = curImage.equals(PASS_ICON) && !outcome.equals(ApexTestOutcome.Pass);
    boolean worseThanWarning =
        curImage.equals(WARNING_ICON)
            && !outcome.equals(ApexTestOutcome.Pass)
            && !outcome.equals(ApexTestOutcome.Skip);
    if (worseThanPass || worseThanWarning) {
      setColorAndIconForNode(node, outcome);
    }
  }

  /**
   * Create a default TreeItem for a test method.
   *
   * @param classNode
   * @param testResult
   * @param className
   * @return TreeItem for test method
   */
  private TreeItem createTestMethodTreeItem(
      TreeItem classNode, ApexTestResult testResult, String className) {
    TreeItem newTestMethodNode = new TreeItem(classNode, SWT.NONE);
    newTestMethodNode.setText(testResult.getMethodName());
    newTestMethodNode.setData(TREEDATA_TEST_RESULT, testResult);

    ApexCodeLocation location =
        getCodeLocationForTestMethod(
            newTestMethodNode, className, testResult.getMethodName(), testResult.getStackTrace());
    newTestMethodNode.setData(TREEDATA_CODE_LOCATION, location);

    return newTestMethodNode;
  }

  /**
   * Get the code location of a test method. If there isn't one, we default to the code location of
   * the test class.
   *
   * @param treeItem
   * @param className
   * @param methodName
   * @param stackTrace
   * @return ApexCodeLocation
   */
  private ApexCodeLocation getCodeLocationForTestMethod(
      TreeItem treeItem, String className, String methodName, String stackTrace) {
    ApexCodeLocation tmLocation = getLocationFromStackLine(methodName, stackTrace);
    ApexCodeLocation tcLocation =
        (ApexCodeLocation) treeItem.getParentItem().getData(TREEDATA_CODE_LOCATION);
    // If there is no test method location, best effort is to use test class location
    if (Utils.isEmpty(tmLocation)) {
      tmLocation = tcLocation;
    } else {
      IFile file = tcLocation.getFile();
      tmLocation.setFile(file);
    }

    return tmLocation;
  }

  /**
   * Get line and column from a stack trace.
   *
   * @param name
   * @param stackTrace
   * @return ApexCodeLocation
   */
  private ApexCodeLocation getLocationFromStackLine(String name, String stackTrace) {
    if (Utils.isEmpty(name) || Utils.isEmpty(stackTrace)) return null;

    String line = null;
    String column = null;
    try {
      String[] temp = stackTrace.split("line");
      line = temp[1].split(",")[0].trim();
      String c = temp[1].trim();
      column = c.split("column")[1].trim();
      if (Utils.isNotEmpty(column) && column.contains("\n")) {
        column = column.substring(0, column.indexOf("\n"));
      }
    } catch (Exception e) {
    }

    return new ApexCodeLocation(name, line, column);
  }

  /**
   * Find a resource and convert to a file.
   *
   * @param testResources
   * @param classID
   * @return
   */
  private IFile getFileFromId(Map<String, IResource> testResources, String classID) {
    if (Utils.isNotEmpty(classID) && Utils.isNotEmpty(testResources)) {
      IResource testResource = testResources.get(classID);
      if (Utils.isNotEmpty(testResource)) {
        return (IFile) testResource;
      }
    }

    return null;
  }

  /**
   * Expand the TreeItems that did not pass.
   *
   * @param resultsTree
   */
  private void expandProblematicTestClasses(Tree resultsTree) {
    if (Utils.isEmpty(resultsTree)) return;

    for (TreeItem classNode : resultsTree.getItems()) {
      if (!classNode.getImage().equals(PASS_ICON)) {
        classNode.setExpanded(true);
      }
    }
  }

  /**
   * Jump to and highlight a line based on the ApexCodeLocation.
   *
   * @param location
   */
  public void highlightLine(ApexCodeLocation location) {
    if (Utils.isEmpty(location) || location.getFile() == null || !location.getFile().exists()) {
      Utils.openWarn("Highlight Failed", "Unable to highlight test file - file is unknown.");
      return;
    }

    HashMap<String, Integer> map = new HashMap<>();
    map.put(IMarker.LINE_NUMBER, location.getLine());
    try {
      IMarker marker = location.getFile().createMarker(IMarker.TEXT);
      marker.setAttributes(map);
      IDE.openEditor(getSite().getWorkbenchWindow().getActivePage(), marker);
    } catch (Exception e) {
      logger.error("Unable to highlight line.", e);
      Utils.openError(new InvocationTargetException(e), true, "Unable to highlight line.");
    }
  }

  /**
   * Update the test results tabs.
   *
   * @param selectedTreeItem
   * @param selectedTab
   */
  public void updateView(TreeItem selectedTreeItem, String selectedTab) {
    if (Utils.isEmpty(selectedTreeItem)
        || Utils.isEmpty(selectedTab)
        || Utils.isEmpty(runTestComposite)) {
      return;
    }

    // Only clear the right side because user will either select an item from the results tree
    // or a tab. We do not want to clear the tree (on the left side).
    runTestComposite.clearTabs();

    ApexTestResult testResult = (ApexTestResult) selectedTreeItem.getData(TREEDATA_TEST_RESULT);
    // If there is no test result, there is nothing to do on the right hand side so just
    // show the test file
    if (Utils.isEmpty(testResult)) {
      // Get the code location and open the file
      ApexCodeLocation location =
          (ApexCodeLocation) selectedTreeItem.getData(TREEDATA_CODE_LOCATION);
      highlightLine(location);
      return;
    }

    // If there is an ApexTestResult to work with, then check which tab is in focus
    // so we can update lazily.
    switch (selectedTab) {
      case RunTestViewComposite.STACK_TRACE:
        showStackTrace(testResult.getMessage(), testResult.getStackTrace());
        break;
      case RunTestViewComposite.SYSTEM_LOG:
        String systemLogId = testResult.getApexLogId();
        String systemApexLog = tryToGetApexLog(selectedTreeItem, systemLogId);
        showSystemLog(systemApexLog);
        break;
      case RunTestViewComposite.USER_LOG:
        String userLogId = testResult.getApexLogId();
        String userApexLog = tryToGetApexLog(selectedTreeItem, userLogId);
        showUserLog(selectedTreeItem, userApexLog);
        break;
    }

    // Show the file after updating the right hand side
    ApexCodeLocation location = (ApexCodeLocation) selectedTreeItem.getData(TREEDATA_CODE_LOCATION);
    highlightLine(location);
  }

  /**
   * Query an ApexLog with the specified log ID.
   *
   * @param forceProject
   * @param logId
   * @return ApexLog
   */
  public ApexLog getApexLog(ForceProject forceProject, String logId) {
    try {
      initializeConnection(forceProject);

      QueryResult qr = toolingStubExt.query(String.format(QUERY_APEX_LOG, logId));
      if (qr != null && qr.getSize() == 1) {
        ApexLog apexLog = (ApexLog) qr.getRecords()[0];
        return apexLog;
      }
    } catch (ForceRemoteException | ForceConnectionException e) {
    }

    return null;
  }

  /**
   * Fetch the raw body of an ApexLog with the specified log ID.
   *
   * @param forceProject
   * @param logId
   * @return Raw log. Null if something is wrong.
   */
  public String getApexLogBody(ForceProject forceProject, String logId) {
    String rawLog = null;

    try {
      initializeConnection(forceProject);

      PromiseableJob<String> job =
          new ApexLogCommand(
              new HTTPAdapter<>(
                  String.class,
                  new ApexLogTransport(toolingRESTConnection, logId),
                  HTTPMethod.GET));
      job.schedule();

      try {
        job.join();
        rawLog = job.getAnswer();
      } catch (InterruptedException e) {
        logger.error("Failed to get Apex Log", e);
      }
    } catch (ForceConnectionException | ForceRemoteException e) {
      logger.error("Failed to connect to Tooling API", e);
    }

    return rawLog;
  }

  /**
   * Get the body of an ApexLog. If that fails, get the toString of an ApexLog.
   *
   * @param selectedTreeItem
   * @param logId
   * @return A string representation of an ApexLog
   */
  public String tryToGetApexLog(TreeItem selectedTreeItem, String logId) {
    if (Utils.isEmpty(forceProject) || Utils.isEmpty(selectedTreeItem) || Utils.isEmpty(logId))
      return null;

    // Do we already have the log body?
    String apexLogBody = (String) selectedTreeItem.getData(TREEDATA_APEX_LOG_BODY);
    if (Utils.isNotEmpty(apexLogBody)) {
      return apexLogBody;
    }

    // Try to get the log body
    apexLogBody = getApexLogBody(forceProject, logId);
    if (Utils.isNotEmpty(apexLogBody)) {
      // Save it for future uses
      selectedTreeItem.setData(TREEDATA_APEX_LOG_BODY, apexLogBody);
      return apexLogBody;
    }

    // There is no ApexLog body, so try to retrieve a saved ApexLog
    ApexLog apexLog = (ApexLog) selectedTreeItem.getData(TREEDATA_APEX_LOG);
    if (Utils.isNotEmpty(apexLog)) {
      return apexLog.toString();
    }

    // Try to get the ApexLog object
    apexLog = getApexLog(forceProject, logId);
    selectedTreeItem.setData(TREEDATA_APEX_LOG, apexLog);
    return (Utils.isNotEmpty(apexLog) ? apexLog.toString() : null);
  }

  /**
   * Update the Stack Trace tab with the given error message & stack trace.
   *
   * @param message
   * @param stackTrace
   */
  public void showStackTrace(String message, String stackTrace) {
    if (Utils.isNotEmpty(runTestComposite)) {
      StringBuilder data = new StringBuilder();

      if (Utils.isNotEmpty(message)) {
        String newLine = System.getProperty("line.separator");
        data.append(message + newLine + newLine);
      }

      if (Utils.isNotEmpty(stackTrace)) {
        data.append(stackTrace);
      }

      runTestComposite.setStackTraceArea(data.toString());
    }
  }

  /**
   * Update the System Debug Log tab with the given log.
   *
   * @param log
   */
  public void showSystemLog(String log) {
    if (Utils.isNotEmpty(runTestComposite) && Utils.isNotEmpty(log)) {
      runTestComposite.setSystemLogsTextArea(log);
    }
  }

  /**
   * Update the User Debug Log tab with a filtered log.
   *
   * @param selectedTreeItem
   * @param log
   */
  public void showUserLog(TreeItem selectedTreeItem, String log) {
    if (Utils.isEmpty(selectedTreeItem) || Utils.isEmpty(runTestComposite)) {
      return;
    }

    // Do we already have a filtered log?
    String userDebugLog = (String) selectedTreeItem.getData(TREEDATA_APEX_LOG_USER_DEBUG);
    if (Utils.isNotEmpty(userDebugLog)) {
      runTestComposite.setUserLogsTextArea(userDebugLog);
      return;
    }

    // Filter the given log with only DEBUG statements
    if (Utils.isNotEmpty(log) && log.contains("DEBUG")) {
      userDebugLog = "";
      String[] newDateWithSperators = log.split("\\|");
      for (int index = 0; index < newDateWithSperators.length; index++) {
        String newDateWithSperator = newDateWithSperators[index];
        if (newDateWithSperator.contains("USER_DEBUG")) {
          String debugData = newDateWithSperators[index + 3];
          debugData = debugData.substring(0, debugData.lastIndexOf('\n'));
          userDebugLog += "\n" + debugData + "\n";
        }
      }
      // Save it for future uses
      selectedTreeItem.setData(TREEDATA_APEX_LOG_USER_DEBUG, userDebugLog);
      // Update the tab
      runTestComposite.setUserLogsTextArea(userDebugLog);
    }
  }

  /**
   * Initialize Tooling connection.
   *
   * @param forceProject
   * @throws ForceConnectionException
   * @throws ForceRemoteException
   */
  private void initializeConnection(ForceProject forceProject)
      throws ForceConnectionException, ForceRemoteException {
    if (toolingRESTConnection != null && toolingStubExt != null) return;

    toolingRESTConnection = new HTTPConnection(forceProject, TOOLING_ENDPOINT);
    toolingRESTConnection.initialize();
    toolingStubExt =
        ContainerDelegate.getInstance()
            .getFactoryLocator()
            .getToolingFactory()
            .getToolingStubExt(forceProject);
  }

  public void setProject(IProject project) {
    this.project = project;
    this.runTestComposite.setProject(project);
    this.runTestComposite.enableComposite();
  }

  public IProject getProject() {
    return project;
  }

  public RunTestViewComposite getRunTestComposite() {
    return runTestComposite;
  }

  @Override
  public void dispose() {
    super.dispose();
    getSite().getPage().removeSelectionListener(fPostSelectionListener);
  }

  @Override
  public void createPartControl(Composite parent) {
    runTestComposite = new RunTestViewComposite(parent, SWT.NONE, this);
    setPartName(VIEW_NAME);
    setTitleImage(getImage());

    UIUtils.setHelpContext(runTestComposite, this.getClass().getSimpleName());
  }

  @Override
  public void setFocus() {
    if (Utils.isNotEmpty(runTestComposite)) {
      runTestComposite.setFocus();
    }
  }

  private void setSelectionListener() {
    fPostSelectionListener =
        new ISelectionListener() {
          @Override
          public void selectionChanged(IWorkbenchPart part, ISelection selection) {
            project = getProjectService().getProject(selection);
            if (selection instanceof IStructuredSelection) {
              IStructuredSelection ss = (IStructuredSelection) selection;
              Object selElement = ss.getFirstElement();
              if (selElement instanceof IResource) {
                setProject(((IResource) selElement).getProject());
              }
            }
          }
        };
  }
}
 public CustomObjectComponentNode(String name) {
   super(name);
   image = ForceImages.get(ForceImages.CUSTOMOBJECT_NODE);
   retrieved = true;
 }
 @Override
 public Image getImage() {
   return ForceImages.get(ForceImages.APEX_GLOBAL_CLASS);
 }