Java tutorial
/******************************************************************************* * Copyright (c) 2015 Salesforce.com, inc.. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Salesforce.com, inc. - initial API and implementation ******************************************************************************/ package com.salesforce.ide.ui.views.runtest; import java.io.IOException; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; import org.apache.log4j.Logger; import org.eclipse.core.filebuffers.FileBuffers; import org.eclipse.core.filebuffers.ITextFileBuffer; import org.eclipse.core.filebuffers.ITextFileBufferManager; import org.eclipse.core.filebuffers.LocationKind; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jface.resource.FontRegistry; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; import org.eclipse.ui.ISelectionListener; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.ide.IDE; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.salesforce.ide.apex.internal.core.ApexSourceUtils; import com.salesforce.ide.core.internal.context.ContainerDelegate; import com.salesforce.ide.core.internal.utils.DialogUtils; import com.salesforce.ide.core.internal.utils.QualifiedNames; import com.salesforce.ide.core.internal.utils.ResourceProperties; import com.salesforce.ide.core.internal.utils.Utils; import com.salesforce.ide.core.model.ApexCodeLocation; import com.salesforce.ide.core.project.ForceProject; import com.salesforce.ide.core.project.MarkerUtils; import com.salesforce.ide.core.remote.ForceConnectionException; import com.salesforce.ide.core.remote.ForceRemoteException; import com.salesforce.ide.core.remote.HTTPAdapter; import com.salesforce.ide.core.remote.HTTPAdapter.HTTPMethod; import com.salesforce.ide.core.remote.HTTPConnection; import com.salesforce.ide.core.remote.ToolingStubExt; import com.salesforce.ide.core.remote.tooling.ApexCodeCoverageAggregate.*; import com.salesforce.ide.core.remote.tooling.ApexLog.*; import com.salesforce.ide.core.remote.tooling.Limits.*; import com.salesforce.ide.core.remote.tooling.RunTests.*; import com.salesforce.ide.core.remote.tooling.ToolingQueryCommand; import com.salesforce.ide.core.remote.tooling.ToolingQueryTransport; import com.salesforce.ide.core.remote.tooling.TraceFlagUtil; import com.salesforce.ide.ui.internal.utils.UIConstants; import com.salesforce.ide.ui.internal.utils.UIUtils; import com.salesforce.ide.ui.views.BaseViewPart; import com.sforce.soap.tooling.sobject.AggregateResult; import com.sforce.soap.tooling.sobject.ApexLog; import com.sforce.soap.tooling.ApexLogLevel; import com.sforce.soap.tooling.sobject.ApexOrgWideCoverage; import com.sforce.soap.tooling.ApexTestOutcome; import com.sforce.soap.tooling.sobject.ApexTestQueueItem; import com.sforce.soap.tooling.sobject.ApexTestResult; import com.sforce.soap.tooling.AsyncApexJobStatus; import com.sforce.soap.tooling.LogCategory; import com.sforce.soap.tooling.QueryResult; import com.sforce.soap.tooling.sobject.SObject; /** * Responsible for running tests, getting results, and updating the UI with the test results. * The actual view is generated by RunTestsViewComposite.java. * * @author jwidjaja * */ public class RunTestsView extends BaseViewPart { private static final class ApexTestResultComparator implements Comparator<ApexTestResult> { @Override public int compare(ApexTestResult tr1, ApexTestResult tr2) { int compareDir = tr1.getApexClass().getName().compareTo(tr2.getApexClass().getName()); if (compareDir == 0) { return tr1.getMethodName().compareTo(tr2.getMethodName()); } return compareDir; } } private static final Logger logger = Logger.getLogger(RunTestsView.class); private static RunTestsView INSTANCE = null; @VisibleForTesting public ReentrantLock lock = new ReentrantLock(); @VisibleForTesting public ForceProject forceProject = null; @VisibleForTesting public ToolingStubExt toolingStubExt = null; private RunTestsViewComposite runTestComposite = null; private IProject project = null; private HTTPConnection toolingRESTConnection = null; private ISelectionListener fPostSelectionListener = null; private TraceFlagUtil tfUtil = null; private boolean shouldCreateTraceFlag = false; public RunTestsView() { super(); setSelectionListener(); INSTANCE = this; } public static RunTestsView 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 = (RunTestsView) PlatformUI.getWorkbench().getActiveWorkbenchWindow() .getActivePage().showView(UIConstants.RUN_TEST_VIEW_ID); } catch (Exception e) { logger.error("Failed to get Apex Test Results view", 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()); } /** * Check if there is an existing active Trace Flag. */ public boolean hasExistingTraceFlag(IProject project) { ForceProject fp = materializeForceProject(project); TraceFlagUtil tf = getTraceFlagUtil(fp); return tf.hasActiveTraceFlag(); } /** * Show error message to user. * TODO: Just throw the exception? Double check all logger.error */ private void throwErrorMsg(final String title, final String solution) { if (Utils.isNotEmpty(title) && Utils.isNotEmpty(solution)) { Display display = PlatformUI.getWorkbench().getDisplay(); display.asyncExec(new Runnable() { @Override public void run() { DialogUtils.getInstance().okMessage(title, solution); } }); } } /** * Run the tests, get the results, and update the UI. */ @VisibleForTesting public void runTests(final IProject project, String testsInJson, boolean shouldUseSuites, boolean isAsync, boolean isDebugging, boolean hasExistingTraceFlag, boolean shouldCreateTraceFlag, Map<LogCategory, ApexLogLevel> logLevels, IProgressMonitor monitor) { forceProject = materializeForceProject(project); if (Utils.isEmpty(forceProject)) { logger.error("Unable to find Force.com project"); return; } lock.lock(); tfUtil = getTraceFlagUtil(forceProject); String debugLevelId = null, traceFlagId = null; try { prepareForRunningTests(project); this.shouldCreateTraceFlag = shouldCreateTraceFlag; // Set user TraceFlag if launch config enabled logging and there isn't // an existing TraceFlag if (this.shouldCreateTraceFlag && !hasExistingTraceFlag) { String userName = forceProject.getUserName(); String userId = tfUtil.getUserId(userName); debugLevelId = tfUtil.insertDebugLevel( RunTestsConstants.DEBUG_LEVEL_NAME + System.currentTimeMillis(), logLevels); traceFlagId = tfUtil.insertTraceFlag(userId, RunTestsConstants.TF_LENGTH_MINS, debugLevelId); tfUtil.automateTraceFlagExtension(traceFlagId, RunTestsConstants.TF_INTERVAL_MINS, RunTestsConstants.TF_LENGTH_MINS); } if (!monitor.isCanceled()) { // Submit the tests for execution String enqueueResult = enqueueTests(testsInJson, shouldUseSuites, isAsync, isDebugging); Map<IResource, List<String>> testResources = findTestClasses(project); if (Utils.isNotEmpty(enqueueResult) && isAsync) { // If it's an async run, the enqueue result is an async test run ID, so we poll for test results getAsyncTestResults(enqueueResult, testResources, monitor); // Display code coverage from ApexCodeCoverageAggregate & ApexOrgWideCoverage displayCodeCoverage(); } else if (Utils.isNotEmpty(enqueueResult) && !isAsync) { /* * Sync test runs do create ApexTestResult objects, but there's no way to query the * right ones for this test run because they don't have a sync test run ID, only async * test run ID. So, we have to rely on the response of runTestsSynchronous. */ ObjectMapper mapper = new ObjectMapper(); RunTestsSyncResponse testResults; try { testResults = mapper.readValue(enqueueResult, RunTestsSyncResponse.class); // Test results are returned all at once updateProgress(0, testResults.getNumTestsRun(), testResults.getNumTestsRun()); // Tests were submitted in alphabetical order. We're relying on the // server to return test results in the same order to avoid sorting // the results processSyncTestResults(project, testResources, testResults); // Display code coverage from ApexCodeCoverageAggregate & ApexOrgWideCoverage displayCodeCoverage(); } catch (IOException e) { logger.error(String.format("Problem reading test result: %s", enqueueResult)); } } } } catch (Exception e) { logger.error("Unexpected error", e); } finally { finishRunningTests(); if (lock.isLocked()) { lock.unlock(); } // Clean up TraceFlag if it was created if (this.shouldCreateTraceFlag && !hasExistingTraceFlag) { tfUtil.removeTraceFlagJobs(); tfUtil.deleteTraceflagAndDebugLevel(traceFlagId, debugLevelId); } } } /** * Clear the test results view and disable Clear button. */ @VisibleForTesting public void prepareForRunningTests(final IProject project) { Display display = PlatformUI.getWorkbench().getDisplay(); display.syncExec(new Runnable() { @Override public void run() { setProject(project); runTestComposite.clearAll(); runTestComposite.setClearButton(false); } }); } /** * Enable Clear button. */ public void finishRunningTests() { Display display = PlatformUI.getWorkbench().getDisplay(); display.syncExec(new Runnable() { @Override public void run() { runTestComposite.setClearButton(true); } }); } /** * Get a ForceProject from an IProject. */ @VisibleForTesting public ForceProject materializeForceProject(IProject project) { if (Utils.isEmpty(project) || !project.exists()) return null; ForceProject forceProject = ContainerDelegate.getInstance().getServiceLocator().getProjectService() .getForceProject(project); return forceProject; } /** * Create a new TraceFlagUtil. */ @VisibleForTesting public TraceFlagUtil getTraceFlagUtil(ForceProject forceProject) { return new TraceFlagUtil(forceProject); } /** * Enqueue a tests array to Tooling's runTestsAsynchronous/runTestsSynchronous. * @return The test run ID if valid async run. * The test results if valid sync run. Null otherwise. */ @VisibleForTesting public String enqueueTests(String testsInJson, boolean shouldUseSuites, boolean isAsync, boolean isDebugging) { String response = null; if (Utils.isEmpty(forceProject)) { return null; } try { int timeoutVal = getConnTimeoutVal(isAsync, isDebugging); initializeConnection(forceProject, timeoutVal); RunTestsCommand job = getRunTestsCommand(testsInJson, isAsync); job.schedule(); job.join(); response = job.getAnswer(); if (job.wasError()) { String enqueueErr = job.getErrorMsg(); if (shouldUseSuites && Utils.isNotEmpty(enqueueErr) && enqueueErr.contains("Please enter valid suiteids")) { enqueueErr = Messages.View_ErrorInvalidSuites; } logger.error(String.format("Failed to enqueue tests. Tests array: %s", testsInJson)); throwErrorMsg(Messages.View_ErrorStartingTestsTitle, enqueueErr); } } catch (Exception e) { logger.error(String.format("Tests array: %s. Error message: %s", testsInJson, e.getMessage())); throwErrorMsg(Messages.View_ErrorStartingTestsTitle, e.getMessage()); } return response; } /** * Get connection timeout value depending on * test mode and debugging status. * @return Timeout value */ @VisibleForTesting public int getConnTimeoutVal(boolean isAsync, boolean isDebugging) { return (isAsync ? RunTestsConstants.ASYNC_TIMEOUT : (isDebugging ? RunTestsConstants.SYNC_WITH_DEBUG_TIMEOUT : RunTestsConstants.SYNC_WITHOUT_DEBUG_TIMEOUT)); } /** * Create the job to submit to Tooling API's run tests endpoint. * @return Promiseable job */ @VisibleForTesting public RunTestsCommand getRunTestsCommand(String testsInJson, boolean isAsync) { return new RunTestsCommand(new HTTPAdapter<>(String.class, new RunTestsTransport(toolingRESTConnection, isAsync), HTTPMethod.POST), testsInJson); } /** * Retrieve test results for the given test run ID. * @return List of ApexTestResult */ @VisibleForTesting public void getAsyncTestResults(String testRunId, final Map<IResource, List<String>> testResources, IProgressMonitor monitor) { if (Utils.isEmpty(forceProject) || Utils.isEmpty(testRunId)) return; testRunId = testRunId.replace("\"", ""); try { initializeConnection(forceProject); // Get remaining daily API requests Limit dailyApiRequests = getApiLimit(forceProject, LimitsCommand.Type.DailyApiRequests); if (dailyApiRequests == null) { logger.error("Failed to get DailyApiRequests limit"); return; } float apiRequestsRemaining = (dailyApiRequests.getRemaining() * 100.0f) / dailyApiRequests.getMax(); if (apiRequestsRemaining <= 0) { logger.error("Not enough DailyApiRequests"); return; } // Poll for remaining test cases to be executed // 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. Integer totalItems = queryTotalQueueItems(testRunId); int processedItems = 0; while (processedItems < totalItems) { processedItems = queryProcessedQueueItem(testRunId); List<ApexTestResult> testResults = queryTestResults(testRunId); // Update progress bar and results view if new results came in updateProgress(0, totalItems, processedItems); Collections.sort(testResults, new ApexTestResultComparator()); processAsyncTestResults(testResources, testResults, processedItems == totalItems); // 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(totalItems - processedItems, apiRequestsRemaining); Thread.sleep(wait); } } catch (Exception e) { logger.error("Failed to get test results", e); throwErrorMsg(Messages.View_ErrorGetAsyncTestResultsTitle, Messages.View_ErrorGetAsyncTestResultsSolution); } } private List<ApexTestResult> queryTestResults(String testRunId) throws ForceRemoteException { List<ApexTestResult> testResults = Lists.newLinkedList(); // Query for test results (one ApexTestResult object per test method) QueryResult qr = toolingStubExt.query(String.format(RunTestsConstants.QUERY_TESTRESULT, testRunId)); if (qr != null && qr.getSize() > 0) { // Even though the query specifies the sort order, getRecords() messages up the order. // Need to sort before displaying the results for (SObject sObj : qr.getRecords()) { ApexTestResult testResult = (ApexTestResult) sObj; testResults.add(testResult); } } return testResults; } // Number of items that have been processed (i.,e anything no longer in queue so success, failure, etc). @VisibleForTesting public int queryProcessedQueueItem(String testRunId) throws ForceRemoteException { int processedItems; QueryResult processedQuery = toolingStubExt .query(String.format(RunTestsConstants.QUERY_APEX_TEST_QUEUE_ITEM_PROCESSED_COUNT, testRunId)); AggregateResult processedAgg = (AggregateResult) processedQuery.getRecords()[0]; processedItems = (Integer) processedAgg.getField("total"); return processedItems; } // Number of items that have been designated for processing. @VisibleForTesting public Integer queryTotalQueueItems(String testRunId) throws ForceRemoteException { QueryResult totalQuery = toolingStubExt .query(String.format(RunTestsConstants.QUERY_APEX_TEST_QUEUE_ITEM_COUNT, testRunId)); AggregateResult totalAgg = (AggregateResult) totalQuery.getRecords()[0]; Integer totalItems = (Integer) totalAgg.getField("total"); return totalItems; } /** * Get a specific API Limit * @see Limit.java */ @VisibleForTesting public Limit getApiLimit(ForceProject forceProject, LimitsCommand.Type type) { try { initializeConnection(forceProject); LimitsCommand job = new LimitsCommand( new HTTPAdapter<>(String.class, new LimitsTransport(toolingRESTConnection), HTTPMethod.GET)); job.schedule(); try { job.join(); String limitsResponse = job.getAnswer(); if (job.wasError()) { logger.error(String.format("Failed to get API limits. Error message: %s", job.getErrorMsg())); } Map<String, Limit> limits = job.parseLimits(limitsResponse); if (limits != null && limits.size() > 0) { return limits.get(type.toString()); } } catch (InterruptedException e) { logger.error("Failed to get API limits", 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 ApexTestQueueItems 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. * @return A poll interval */ @VisibleForTesting public int getPollInterval(int queuedItemsRemaining, float apiRequestsRemaining) { int intervalA = RunTestsConstants.POLL_SLOW, intervalB = RunTestsConstants.POLL_SLOW; if (queuedItemsRemaining <= 2) { intervalA = RunTestsConstants.POLL_FAST; } else if (queuedItemsRemaining <= 10) { intervalA = RunTestsConstants.POLL_MED; } else { intervalA = RunTestsConstants.POLL_SLOW; } if (apiRequestsRemaining <= 25f) { intervalB = RunTestsConstants.POLL_SLOW; } else if (apiRequestsRemaining <= 50f) { intervalB = RunTestsConstants.POLL_MED; } else { intervalB = RunTestsConstants.POLL_FAST; } return (intervalA + intervalB) / 2; } /** * Update the progress bar to show user the number of tests finished. */ @VisibleForTesting 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. */ @VisibleForTesting public boolean abortTestRun(String testRunId) { if (Utils.isEmpty(forceProject) || Utils.isEmpty(testRunId)) { return false; } try { initializeConnection(forceProject); // Get all ApexTestQueueItem in the test run QueryResult qr = toolingStubExt .query(String.format(RunTestsConstants.QUERY_APEX_TEST_QUEUE_ITEM, testRunId)); if (Utils.isEmpty(qr) || qr.getSize() == 0) return false; // Update status to Aborted 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); } } // Update in chunks because there is a limit to how many we can update in one call if (!abortedList.isEmpty()) { for (List<ApexTestQueueItem> subList : Lists.partition(abortedList, 200)) { ApexTestQueueItem[] abortedArray = subList.toArray(new ApexTestQueueItem[subList.size()]); toolingStubExt.update(abortedArray); } return true; } } catch (ForceConnectionException | ForceRemoteException e) { logger.error("Failed to abort test run", e); } return false; } /** * Update the UI with the test results for an asynchronous test run. */ @VisibleForTesting public void processAsyncTestResults(final Map<IResource, List<String>> testResources, final List<ApexTestResult> testResults, final boolean expandFailedResults) { if (Utils.isEmpty(testResources) || Utils.isEmpty(testResults)) { return; } Display display = PlatformUI.getWorkbench().getDisplay(); display.syncExec(new Runnable() { @Override public void run() { // Map of tree items whose key is apex class id and the value is the tree item Map<String, TreeItem> testClassNodes = Maps.newLinkedHashMap(); FontRegistry registry = new FontRegistry(); Font boldFont = registry.getBold(Display.getCurrent().getSystemFont().getFontData()[0].getName()); runTestComposite.clearAllExceptProgress(); Tree resultsTree = runTestComposite.getTree(); // 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(); if (!testClassNodes.containsKey(classId)) { TreeItem newClassNode = createTestClassTreeItem(resultsTree, testResources, boldFont, classId); testClassNodes.put(classId, newClassNode); } // Add the a test method tree node to the test class tree node TreeItem classNode = testClassNodes.get(classId); String 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()); } if (expandFailedResults) { // Expand the test classes that did not pass expandProblematicTestClasses(resultsTree); } } }); } /** * Update the UI with the test results for an synchronous test run. */ @VisibleForTesting public void processSyncTestResults(final IProject project, final Map<IResource, List<String>> testResources, final RunTestsSyncResponse testResults) { if (Utils.isEmpty(project) || Utils.isEmpty(testResources) || Utils.isEmpty(testResults)) { return; } Display display = PlatformUI.getWorkbench().getDisplay(); display.syncExec(new Runnable() { @Override public void run() { // Map of tree items whose key is apex class id and the value is the tree item Map<String, TreeItem> testClassNodes = Maps.newLinkedHashMap(); FontRegistry registry = new FontRegistry(); Font boldFont = registry.getBold(Display.getCurrent().getSystemFont().getFontData()[0].getName()); // Reset tree Tree resultsTree = runTestComposite.getTree(); resultsTree.removeAll(); for (RunTestsSyncSuccess testPassed : testResults.getSuccesses()) { // Create or find the tree node for the test class final String classId = testPassed.getId(); final String className = testPassed.getName(); if (!testClassNodes.containsKey(classId)) { TreeItem newClassNode = createTestClassTreeItem(resultsTree, testResources, boldFont, classId); testClassNodes.put(classId, newClassNode); } // Add the a test method tree node to the test class tree node TreeItem classNode = testClassNodes.get(classId); // Create a tree item for the test method and save the test result TreeItem newTestMethodNode = createTestMethodTreeItem(classNode, className, testPassed.getMethodName(), "", "", testResults.getApexLogId()); // Set the color and icon of test method tree node based on test outcome setColorAndIconForNode(newTestMethodNode, ApexTestOutcome.Pass); } for (RunTestsSyncFailure testFailed : testResults.getFailures()) { // Create or find the tree node for the test class final String classId = testFailed.getId(); final String className = testFailed.getName(); if (!testClassNodes.containsKey(classId)) { TreeItem newClassNode = createTestClassTreeItem(resultsTree, testResources, boldFont, classId); testClassNodes.put(classId, newClassNode); } // Add the a test method tree node to the test class tree node TreeItem classNode = testClassNodes.get(classId); // Create a tree item for the test method and save the test result TreeItem newTestMethodNode = createTestMethodTreeItem(classNode, className, testFailed.getMethodName(), testFailed.getMessage(), testFailed.getStackTrace(), testResults.getApexLogId()); // Set the color and icon of test method tree node based on test outcome setColorAndIconForNode(newTestMethodNode, ApexTestOutcome.Fail); // 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, ApexTestOutcome.Fail); } // Expand the test classes that did not pass expandProblematicTestClasses(resultsTree); } }); } /** * Display org wide and individual class/trigger code coverage * from ApexOrgWideCoverage & ApexCodeCoverageAgg */ @VisibleForTesting public void displayCodeCoverage() { if (Utils.isEmpty(forceProject) || Utils.isEmpty(runTestComposite)) return; final List<CodeCovResult> ccResults = Lists.newArrayList(); ApexOrgWideCoverage orgWide = getApexOrgWideCoverage(); ApexCodeCoverageAggregateResponse codeCovs = getApexCodeCoverageAgg(); IProject proj = forceProject.getProject(); // Get a list of existing Apex classes & triggers List<IResource> resources = ApexSourceUtils.INSTANCE.findLocalSourcesInProject(proj); resources = ApexSourceUtils.INSTANCE.filterSourcesByClassOrTrigger(resources); // Save overall code coverage Integer orgWidePercent = Utils.isNotEmpty(orgWide) ? orgWide.getPercentCovered() : 0; CodeCovResult ccResult = new CodeCovResult(Messages.View_CodeCoverageOverall, null, orgWidePercent, null, null); ccResults.add(ccResult); for (Record codeCov : codeCovs.records) { // Get name, percent and lines covered final String classOrTriggerName = codeCov.ApexClassOrTrigger.Name; Integer linesCovered = codeCov.NumLinesCovered; Integer total = linesCovered + codeCov.NumLinesUncovered; Integer percent = (int) Math.round(linesCovered * 100.0 / total); // Find the correct resource for the given classOrTriggerName FluentIterable<IResource> curRes = FluentIterable.from(resources).filter(new Predicate<IResource>() { @Override public boolean apply(IResource res) { if (res.getName().contains(classOrTriggerName)) { return true; } return false; } }); // Show code coverage markers on Apex class/trigger if (curRes != null && !curRes.isEmpty()) { try { // Save code coverage info with resource ApexCodeLocation location = findClass(curRes.get(0)); ccResults.add(new CodeCovResult(classOrTriggerName, location, percent, linesCovered, total)); applyCodeCoverageMarker(curRes.get(0), codeCov.Coverage.uncoveredLines); } catch (CoreException | BadLocationException e) { logger.error("Failed to apply code coverage warnings for " + classOrTriggerName, e); } } else { // Save code coverage info without resource ccResults.add(new CodeCovResult(classOrTriggerName, null, percent, linesCovered, total)); logger.error(String.format("Failed to find resource %s for code coverage", classOrTriggerName)); } } Display display = PlatformUI.getWorkbench().getDisplay(); display.syncExec(new Runnable() { @Override public void run() { // Update Apex Test Results view with code coverage runTestComposite.setCodeCoverage(ccResults); } }); } /** * For each uncovered line in the resource, find the start and end of the line * and annotate it as not covered. */ private void applyCodeCoverageMarker(IResource resource, List<Integer> uncoveredLines) throws CoreException, BadLocationException { IFile iFile = (IFile) resource; ITextFileBufferManager iTextFileBufferManager = FileBuffers.getTextFileBufferManager(); iTextFileBufferManager.connect(iFile.getFullPath(), LocationKind.IFILE, new NullProgressMonitor()); ITextFileBuffer iTextFileBuffer = iTextFileBufferManager.getTextFileBuffer(iFile.getFullPath(), LocationKind.IFILE); IDocument iDoc = iTextFileBuffer.getDocument(); iTextFileBufferManager.disconnect(iFile.getFullPath(), LocationKind.IFILE, new NullProgressMonitor()); for (Integer uncoveredLine : uncoveredLines) { int start = iDoc.getLineOffset(uncoveredLine - 1); int end = iDoc.getLineLength(uncoveredLine - 1); MarkerUtils.getInstance().applyCodeCoverageWarningMarker(resource, uncoveredLine, start, start + end, Messages.View_LineNotCovered); } } /** * Create a default TreeItem for a test class. * @return TreeItem for test class */ private TreeItem createTestClassTreeItem(Tree parent, Map<IResource, List<String>> testResources, Font font, String classId) { 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); // Test result only has test class ID. Find the test class name mapped to that ID to display in UI IResource testResource = getResourceFromId(testResources, classId); String className = Utils.isNotEmpty(testResource) ? testResource.getName() : classId; newClassNode.setText(className); // Save the associated file in the tree item if (Utils.isNotEmpty(testResource)) { // For test classes, point to the class declaratio. Fallback to the first // line & column ApexCodeLocation location = findClass(testResource); newClassNode.setData(RunTestsConstants.TREEDATA_CODE_LOCATION, location); Map<String, ApexCodeLocation> testMethodLocs = findTestMethods(testResource); if (testMethodLocs != null && testMethodLocs.size() > 0) { newClassNode.setData(RunTestsConstants.TREEDATA_TEST_METHOD_LOCS, testMethodLocs); } } return newClassNode; } /** * Set color and icon for a test method's TreeItem. */ 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(RunTestsConstants.PASS_COLOR)); node.setImage(RunTestsConstants.PASS_ICON); } else if (outcome.equals(ApexTestOutcome.Skip)) { node.setForeground(display.getSystemColor(RunTestsConstants.WARNING_COLOR)); node.setImage(RunTestsConstants.WARNING_ICON); } else { node.setForeground(display.getSystemColor(RunTestsConstants.FAILURE_COLOR)); node.setImage(RunTestsConstants.FAILURE_ICON); } } /** * Update the color & icon of a TreeItem only if the given outcome is worse than * what the TreeItem already indicates. */ private void setColorAndIconForTheWorse(TreeItem node, ApexTestOutcome outcome) { if (Utils.isEmpty(node) || Utils.isEmpty(outcome)) return; Image curImage = node.getImage(); boolean worseThanPass = curImage.equals(RunTestsConstants.PASS_ICON) && !outcome.equals(ApexTestOutcome.Pass); boolean worseThanWarning = curImage.equals(RunTestsConstants.WARNING_ICON) && !outcome.equals(ApexTestOutcome.Pass) && !outcome.equals(ApexTestOutcome.Skip); if (worseThanPass || worseThanWarning) { setColorAndIconForNode(node, outcome); } } private ApexCodeLocation findClass(IResource resource) { return ApexSourceUtils.INSTANCE.findClassLocInFile(resource); } @VisibleForTesting public Map<IResource, List<String>> findTestClasses(IProject project) { return ApexSourceUtils.INSTANCE.findTestClassesInProject(project); } private Map<String, ApexCodeLocation> findTestMethods(IResource resource) { return ApexSourceUtils.INSTANCE.findTestMethodLocsInFile(resource); } /** * Create a TreeItem for a test method from an async test run. * @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(RunTestsConstants.TREEDATA_APEX_LOG_ID, testResult.getApexLogId()); newTestMethodNode.setData(RunTestsConstants.TREEDATA_RESULT_MESSAGE, testResult.getMessage()); newTestMethodNode.setData(RunTestsConstants.TREEDATA_RESULT_STACKTRACE, testResult.getStackTrace()); ApexCodeLocation location = getCodeLocationForTestMethod(newTestMethodNode, classNode, className, testResult.getMethodName(), testResult.getStackTrace()); newTestMethodNode.setData(RunTestsConstants.TREEDATA_CODE_LOCATION, location); return newTestMethodNode; } /** * Create a TreeItem for a test method from an sync test run * @return TreeItem for test method */ private TreeItem createTestMethodTreeItem(TreeItem classNode, String className, String methodName, String message, String stackTrace, String apexLogId) { TreeItem newTestMethodNode = new TreeItem(classNode, SWT.NONE); newTestMethodNode.setText(methodName); newTestMethodNode.setData(RunTestsConstants.TREEDATA_APEX_LOG_ID, apexLogId); newTestMethodNode.setData(RunTestsConstants.TREEDATA_RESULT_MESSAGE, message); newTestMethodNode.setData(RunTestsConstants.TREEDATA_RESULT_STACKTRACE, stackTrace); ApexCodeLocation location = getCodeLocationForTestMethod(newTestMethodNode, classNode, className, methodName, stackTrace); newTestMethodNode.setData(RunTestsConstants.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. * @return ApexCodeLocation */ private ApexCodeLocation getCodeLocationForTestMethod(TreeItem methodNode, TreeItem classNode, String className, String methodName, String stackTrace) { @SuppressWarnings("unchecked") Map<String, ApexCodeLocation> testMethodLocs = (Map<String, ApexCodeLocation>) classNode .getData(RunTestsConstants.TREEDATA_TEST_METHOD_LOCS); ApexCodeLocation tmLocation = (testMethodLocs != null && testMethodLocs.containsKey(methodName)) ? testMethodLocs.get(methodName) : getLocationFromStackLine(methodName, stackTrace); ApexCodeLocation tcLocation = (ApexCodeLocation) methodNode.getParentItem() .getData(RunTestsConstants.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. * @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. * @return A source file */ @VisibleForTesting public IResource getResourceFromId(Map<IResource, List<String>> testResources, String classID) { if (Utils.isNotEmpty(classID) && Utils.isNotEmpty(testResources)) { for (IResource testResource : testResources.keySet()) { String resourceId = ResourceProperties.getProperty(testResource, QualifiedNames.QN_ID); if (resourceId.equals(classID)) { return testResource; } } } return null; } /** * Expand the TreeItems that did not pass. */ private void expandProblematicTestClasses(Tree resultsTree) { if (Utils.isEmpty(resultsTree)) return; for (TreeItem classNode : resultsTree.getItems()) { if (!classNode.getImage().equals(RunTestsConstants.PASS_ICON)) { classNode.setExpanded(true); } } } /** * Jump to and highlight a line based on the ApexCodeLocation. */ @VisibleForTesting public void highlightLine(ApexCodeLocation location) { if (Utils.isEmpty(location) || location.getFile() == null || !location.getFile().exists()) { throwErrorMsg(Messages.View_ErrorOpenSourceTitle, Messages.View_ErrorOpenSourceSolution); return; } Map<String, Integer> map = Maps.newHashMap(); map.put(IMarker.LINE_NUMBER, location.getLine()); try { IMarker marker = location.getFile().createMarker(IMarker.TEXT); marker.setAttributes(map); IDE.openEditor(getSite().getWorkbenchWindow().getActivePage(), marker); marker.delete(); } catch (Exception e) { logger.error("Unable to highlight line", e); throwErrorMsg(Messages.View_ErrorOpenSourceTitle, Messages.View_ErrorOpenSourceSolution); } } /** * Update the test results tabs. */ public void updateView(TreeItem selectedTreeItem, String selectedTab, boolean openSource) { 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(); // Selecting a tab gives user more details on the test result, but for that to happen, // they must select the test from the results tree first, which is when we will get // the code location and open the source. if (openSource) { ApexCodeLocation location = (ApexCodeLocation) selectedTreeItem .getData(RunTestsConstants.TREEDATA_CODE_LOCATION); highlightLine(location); } // Get the test result String apexLogId = (String) selectedTreeItem.getData(RunTestsConstants.TREEDATA_APEX_LOG_ID); String errorMessage = (String) selectedTreeItem.getData(RunTestsConstants.TREEDATA_RESULT_MESSAGE); String stackTrace = (String) selectedTreeItem.getData(RunTestsConstants.TREEDATA_RESULT_STACKTRACE); // Check which tab is in focus so we can update lazily if (selectedTab.equals(Messages.View_StackTrace)) { // Stack trace only exists in a test failure showStackTrace(errorMessage, stackTrace); } else if (this.shouldCreateTraceFlag && selectedTab.equals(Messages.View_SystemLog)) { String apexLog = tryToGetApexLog(selectedTreeItem, apexLogId); showSystemLog(apexLog); } else if (this.shouldCreateTraceFlag && selectedTab.equals(Messages.View_UserLog)) { String apexLog = tryToGetApexLog(selectedTreeItem, apexLogId); showUserLog(selectedTreeItem, apexLog); } } /** * Query an ApexLog with the specified log ID. * @return ApexLog */ private ApexLog getApexLog(ForceProject forceProject, String logId) { try { initializeConnection(forceProject); QueryResult qr = toolingStubExt.query(String.format(RunTestsConstants.QUERY_APEX_LOG, logId)); if (qr != null && qr.getSize() == 1) { ApexLog apexLog = (ApexLog) qr.getRecords()[0]; return apexLog; } } catch (Exception e) { logger.error("Failed to get Apex log", e); } return null; } /** * Fetch the raw body of an ApexLog with the specified log ID. * @return Raw log. Null if something is wrong. */ private String getApexLogBody(ForceProject forceProject, String logId) { String rawLog = null; try { initializeConnection(forceProject); ApexLogCommand job = new ApexLogCommand(new HTTPAdapter<>(String.class, new ApexLogTransport(toolingRESTConnection, logId), HTTPMethod.GET)); job.schedule(); try { job.join(); rawLog = job.getAnswer(); if (job.wasError()) { logger.error(String.format("Failed to get Apex Log. Error message: %s", job.getErrorMsg())); } } catch (InterruptedException e) { logger.error("Failed to get Apex Log", e); } } catch (Exception 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. * @return A string representation of an ApexLog */ private 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? try { String apexLogBody = (String) selectedTreeItem.getData(RunTestsConstants.TREEDATA_APEX_LOG_BODY); if (Utils.isNotEmpty(apexLogBody)) { return apexLogBody; } } catch (Exception e) { } // Try to get the log body String apexLogBody = getApexLogBody(forceProject, logId); if (Utils.isNotEmpty(apexLogBody)) { // Save it for future uses selectedTreeItem.setData(RunTestsConstants.TREEDATA_APEX_LOG_BODY, apexLogBody); return apexLogBody; } // There is no ApexLog body, so try to retrieve a saved ApexLog try { ApexLog apexLog = (ApexLog) selectedTreeItem.getData(RunTestsConstants.TREEDATA_APEX_LOG); if (Utils.isNotEmpty(apexLog)) { return apexLog.toString(); } } catch (Exception e) { } // Try to get the ApexLog object ApexLog apexLog = getApexLog(forceProject, logId); selectedTreeItem.setData(RunTestsConstants.TREEDATA_APEX_LOG, apexLog); return (Utils.isNotEmpty(apexLog) ? apexLog.toString() : null); } /** * Update the Stack Trace tab with the given error message & stack trace. */ private void showStackTrace(String message, String stackTrace) { if (Utils.isNotEmpty(runTestComposite)) { StringBuilder data = new StringBuilder(); if (Utils.isNotEmpty(message)) { data.append(message + RunTestsConstants.NEW_LINE + RunTestsConstants.NEW_LINE); } if (Utils.isNotEmpty(stackTrace)) { data.append(stackTrace); } runTestComposite.setStackTraceArea(data.toString()); } } /** * Update the System Debug Log tab with the given log. */ private void showSystemLog(String log) { if (Utils.isNotEmpty(runTestComposite) && Utils.isNotEmpty(log)) { runTestComposite.setSystemLogsTextArea(log); } } /** * Update the User Debug Log tab with a filtered log. */ private void showUserLog(TreeItem selectedTreeItem, String log) { if (Utils.isEmpty(selectedTreeItem) || Utils.isEmpty(runTestComposite)) { return; } // Do we already have a filtered log? try { String userDebugLog = (String) selectedTreeItem.getData(RunTestsConstants.TREEDATA_APEX_LOG_USER_DEBUG); if (Utils.isNotEmpty(userDebugLog)) { runTestComposite.setUserLogsTextArea(userDebugLog); return; } } catch (Exception e) { } // Filter the given log with only DEBUG statements if (Utils.isNotEmpty(log) && log.contains("DEBUG")) { String 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(RunTestsConstants.TREEDATA_APEX_LOG_USER_DEBUG, userDebugLog); // Update the tab runTestComposite.setUserLogsTextArea(userDebugLog); } } /** * Get the code coverage aggregate. */ private ApexCodeCoverageAggregateResponse getApexCodeCoverageAgg() { ApexCodeCoverageAggregateResponse results = null; String response = ""; try { initializeConnection(forceProject); ToolingQueryCommand job = getQueryJob(RunTestsConstants.QUERY_APEX_CODE_COVERAGE_AGG); job.schedule(); job.join(); response = job.getAnswer(); if (job.wasError()) { logger.error(String.format("Failed to run query: %s. Error message: %s", RunTestsConstants.QUERY_APEX_CODE_COVERAGE_AGG, job.getErrorMsg())); } ObjectMapper mapper = new ObjectMapper(); results = mapper.readValue(response, ApexCodeCoverageAggregateResponse.class); } catch (Exception e) { logger.error("Failed to get ApexCodeCoverageAggregate. Response is " + response, e); } return results; } /** * Query Tooling API. * @return Promiseable job */ private ToolingQueryCommand getQueryJob(String query) { return new ToolingQueryCommand(new HTTPAdapter<>(String.class, new ToolingQueryTransport(toolingRESTConnection, query), HTTPMethod.GET)); } /** * Get the org wide code coverage. * @return ApexOrgWideCoverage */ private ApexOrgWideCoverage getApexOrgWideCoverage() { try { initializeConnection(forceProject); QueryResult qr = toolingStubExt.query(RunTestsConstants.QUERY_APEX_ORG_WIDE_COVERAGE); if (qr != null && qr.getSize() == 1) { ApexOrgWideCoverage orgWideCov = (ApexOrgWideCoverage) qr.getRecords()[0]; return orgWideCov; } } catch (Exception e) { logger.error("Failed to get ApexOrgWideCoverage", e); } return null; } /** * Initialize Tooling connection. */ @VisibleForTesting public void initializeConnection(ForceProject forceProject) throws ForceConnectionException, ForceRemoteException { toolingRESTConnection = new HTTPConnection(forceProject, RunTestsConstants.TOOLING_ENDPOINT); toolingRESTConnection.initialize(); toolingStubExt = ContainerDelegate.getInstance().getFactoryLocator().getToolingFactory() .getToolingStubExt(forceProject); } /** * Initialize Tooling Connection with timeout. */ @VisibleForTesting public void initializeConnection(ForceProject forceProject, int timeout) throws ForceConnectionException, ForceRemoteException { toolingRESTConnection = new HTTPConnection(forceProject, RunTestsConstants.TOOLING_ENDPOINT, timeout); toolingRESTConnection.initialize(); toolingStubExt = ContainerDelegate.getInstance().getFactoryLocator().getToolingFactory() .getToolingStubExt(forceProject); } private void setProject(IProject project) { this.project = project; this.runTestComposite.setProject(this.project); } @VisibleForTesting public RunTestsViewComposite getRunTestComposite() { return runTestComposite; } @Override public void dispose() { super.dispose(); getSite().getPage().removeSelectionListener(fPostSelectionListener); } @Override public void createPartControl(Composite parent) { runTestComposite = new RunTestsViewComposite(parent, SWT.NONE, this); setPartName(Messages.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()); } } } }; } }