/** * 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); }