/******************************************************************************* * Copyright (c) 2011, 2012 Anton Gorenkov * 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: * Anton Gorenkov - initial API and implementation *******************************************************************************/ package org.eclipse.cdt.testsrunner.internal.ui.view; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.cdt.internal.ui.viewsupport.ColoringLabelProvider; import org.eclipse.cdt.testsrunner.internal.TestsRunnerPlugin; import org.eclipse.cdt.testsrunner.internal.ui.view.actions.CopySelectedTestsAction; import org.eclipse.cdt.testsrunner.internal.ui.view.actions.RedebugSelectedAction; import org.eclipse.cdt.testsrunner.internal.ui.view.actions.RelaunchSelectedAction; import org.eclipse.cdt.testsrunner.internal.ui.view.actions.RerunSelectedAction; import org.eclipse.cdt.testsrunner.internal.ui.view.actions.TestsHierarchyCollapseAllAction; import org.eclipse.cdt.testsrunner.internal.ui.view.actions.TestsHierarchyExpandAllAction; import org.eclipse.cdt.testsrunner.model.IModelVisitor; import org.eclipse.cdt.testsrunner.model.ITestCase; import org.eclipse.cdt.testsrunner.model.ITestItem; import org.eclipse.cdt.testsrunner.model.ITestMessage; import org.eclipse.cdt.testsrunner.model.ITestSuite; import org.eclipse.cdt.testsrunner.model.ITestingSession; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider.IStyledLabelProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.StyledCellLabelProvider; import org.eclipse.jface.viewers.StyledString; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerFilter; import org.eclipse.swt.SWT; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Menu; import org.eclipse.ui.IActionBars; import org.eclipse.ui.IViewSite; import org.eclipse.ui.actions.ActionFactory; /** * Shows the tests hierarchy in a flat or hierarchical view. */ public class TestsHierarchyViewer { /** * The content provider for the tests hierarchy viewer. */ private class TestTreeContentProvider implements ITreeContentProvider { /** * Utility class: recursively collects all the test cases of the * specified test item. * * It is used for flat view of tests hierarchy. */ private class TestCasesCollector implements IModelVisitor { public List<ITestCase> testCases = new ArrayList<ITestCase>(); @Override public void visit(ITestCase testCase) { testCases.add(testCase); } @Override public void visit(ITestMessage testMessage) {} @Override public void visit(ITestSuite testSuite) {} @Override public void leave(ITestSuite testSuite) {} @Override public void leave(ITestCase testCase) {} @Override public void leave(ITestMessage testMessage) {} } @Override public Object[] getChildren(Object parentElement) { return ((ITestItem) parentElement).getChildren(); } @Override public Object[] getElements(Object rootTestSuite) { if (showTestsHierarchy) { return getChildren(rootTestSuite); } else { TestCasesCollector testCasesCollector = new TestCasesCollector(); ((ITestItem)rootTestSuite).visit(testCasesCollector); return testCasesCollector.testCases.toArray(); } } @Override public Object getParent(Object object) { return ((ITestItem) object).getParent(); } @Override public boolean hasChildren(Object object) { return ((ITestItem) object).hasChildren(); } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {} @Override public void dispose() {} } /** * The label provider for the tests hierarchy viewer. */ private class TestLabelProvider extends LabelProvider implements IStyledLabelProvider { /** Images for the test cases with the different statuses. */ private Map<ITestItem.Status, Image> testCaseImages = new HashMap<ITestItem.Status, Image>(); { testCaseImages.put(ITestItem.Status.NotRun, TestsRunnerPlugin.createAutoImage("obj16/test_notrun.gif")); //$NON-NLS-1$ testCaseImages.put(ITestItem.Status.Skipped, TestsRunnerPlugin.createAutoImage("obj16/test_skipped.gif")); //$NON-NLS-1$ testCaseImages.put(ITestItem.Status.Passed, TestsRunnerPlugin.createAutoImage("obj16/test_passed.gif")); //$NON-NLS-1$ testCaseImages.put(ITestItem.Status.Failed, TestsRunnerPlugin.createAutoImage("obj16/test_failed.gif")); //$NON-NLS-1$ testCaseImages.put(ITestItem.Status.Aborted, TestsRunnerPlugin.createAutoImage("obj16/test_aborted.gif")); //$NON-NLS-1$ } /** Running test case image (overrides the test case status image). */ private Image testCaseRunImage = TestsRunnerPlugin.createAutoImage("obj16/test_run.gif"); //$NON-NLS-1$ /** Images for the test suites with the different statuses. */ private Map<ITestItem.Status, Image> testSuiteImages = new HashMap<ITestItem.Status, Image>(); { // NOTE: There is no skipped-icon for test suite, but it seems it is not a problem testSuiteImages.put(ITestItem.Status.NotRun, TestsRunnerPlugin.createAutoImage("obj16/tsuite_notrun.gif")); //$NON-NLS-1$ testSuiteImages.put(ITestItem.Status.Skipped, TestsRunnerPlugin.createAutoImage("obj16/tsuite_notrun.gif")); //$NON-NLS-1$ testSuiteImages.put(ITestItem.Status.Passed, TestsRunnerPlugin.createAutoImage("obj16/tsuite_passed.gif")); //$NON-NLS-1$ testSuiteImages.put(ITestItem.Status.Failed, TestsRunnerPlugin.createAutoImage("obj16/tsuite_failed.gif")); //$NON-NLS-1$ testSuiteImages.put(ITestItem.Status.Aborted, TestsRunnerPlugin.createAutoImage("obj16/tsuite_aborted.gif")); //$NON-NLS-1$ } /** Running test suite image (overrides the test suite status image). */ private Image testSuiteRunImage = TestsRunnerPlugin.createAutoImage("obj16/tsuite_run.gif"); //$NON-NLS-1$ /** Small optimization: the last test item cache */ private ITestItem lastTestItemCache = null; /** Small optimization: test path for the last test item is cache */ private String lastTestItemPathCache = null; @Override public Image getImage(Object element) { Map<ITestItem.Status, Image> imagesMap = null; Image runImage = null; if (element instanceof ITestCase) { imagesMap = testCaseImages; runImage = testCaseRunImage; } else if (element instanceof ITestSuite) { imagesMap = testSuiteImages; runImage = testSuiteRunImage; } if (imagesMap != null) { ITestItem testItem = (ITestItem)element; if (testingSession.getModelAccessor().isCurrentlyRunning(testItem)) { return runImage; } return imagesMap.get(testItem.getStatus()); } return null; } @Override public String getText(Object element) { ITestItem testItem = (ITestItem)element; StringBuilder sb = new StringBuilder(); sb.append(testItem.getName()); if (!showTestsHierarchy) { appendTestItemPath(sb, testItem); } if (showTime) { sb.append(getTestingTimeString(element)); } return sb.toString(); } @Override public StyledString getStyledText(Object element) { ITestItem testItem = (ITestItem)element; StringBuilder labelBuf = new StringBuilder(); labelBuf.append(testItem.getName()); StyledString name = new StyledString(labelBuf.toString()); if (!showTestsHierarchy) { appendTestItemPath(labelBuf, testItem); name = StyledCellLabelProvider.styleDecoratedString(labelBuf.toString(), StyledString.QUALIFIER_STYLER, name); } if (showTime) { String time = getTestingTimeString(element); labelBuf.append(time); name = StyledCellLabelProvider.styleDecoratedString(labelBuf.toString(), StyledString.COUNTER_STYLER, name); } return name; } /** * Appends path to the parent of the specified test item. Also * implements caching of the last path (cause the test item parent is * often the same). * * @param sb string builder to append test item path to * @param testItem specified test item */ private void appendTestItemPath(StringBuilder sb, ITestItem testItem) { ITestSuite testItemParent = testItem.getParent(); if (lastTestItemCache != testItemParent) { lastTestItemCache = testItemParent; lastTestItemPathCache = TestPathUtils.getTestItemPath(lastTestItemCache); } sb.append(MessageFormat.format( UIViewMessages.TestsHierarchyViewer_test_path_format, new Object[] { lastTestItemPathCache } )); } /** * Returns the execution time suffix for the test item. * * @param element test item * @return execution time suffix */ private String getTestingTimeString(Object element) { return (element instanceof ITestItem) ? MessageFormat.format(UIViewMessages.TestsHierarchyViewer_test_time_format, Double.toString(((ITestItem)element).getTestingTime()/1000.0)) : ""; //$NON-NLS-1$ } } /** * Filters passed test cases and test suites. */ private class FailedOnlyFilter extends ViewerFilter { @Override public boolean select(Viewer viewer, Object parentElement, Object element) { return ((ITestItem)element).getStatus().isError(); } } /** Testing session to show hierarchy of. */ private ITestingSession testingSession; /** Main widget. */ private TreeViewer treeViewer; /** Specifies whether test items execution time should be shown in hierarchy. */ private boolean showTime = true; /** Specifies whether tests hierarchy should be shown in flat or hierarchical view. */ private boolean showTestsHierarchy = true; /** Failed only tree filter instance. Created on first demand. */ private FailedOnlyFilter failedOnlyFilter = null; /** System clipboard access to provide copy operations. */ private Clipboard clipboard; // Context menu actions private Action expandAllAction; private Action collapseAllAction; private Action copyAction; private RelaunchSelectedAction rerunAction; private RelaunchSelectedAction redebugAction; public TestsHierarchyViewer(Composite parent, IViewSite viewSite, Clipboard clipboard) { this.clipboard = clipboard; treeViewer = new TreeViewer(parent, SWT.V_SCROLL | SWT.MULTI); treeViewer.setContentProvider(new TestTreeContentProvider()); treeViewer.setLabelProvider(new ColoringLabelProvider(new TestLabelProvider())); initContextMenu(viewSite); } /** * Initializes the viewer context menu. * * @param viewSite view */ private void initContextMenu(IViewSite viewSite) { expandAllAction = new TestsHierarchyExpandAllAction(treeViewer); collapseAllAction = new TestsHierarchyCollapseAllAction(treeViewer); copyAction = new CopySelectedTestsAction(treeViewer, clipboard); rerunAction = new RerunSelectedAction(testingSession, treeViewer); redebugAction = new RedebugSelectedAction(testingSession, treeViewer); MenuManager menuMgr = new MenuManager("#PopupMenu"); //$NON-NLS-1$ menuMgr.addMenuListener(new IMenuListener() { @Override public void menuAboutToShow(IMenuManager manager) { handleMenuAboutToShow(manager); } }); viewSite.registerContextMenu(menuMgr, treeViewer); Menu menu = menuMgr.createContextMenu(treeViewer.getTree()); treeViewer.getTree().setMenu(menu); menuMgr.add(copyAction); menuMgr.add(new Separator()); menuMgr.add(rerunAction); menuMgr.add(redebugAction); menuMgr.add(new Separator()); menuMgr.add(expandAllAction); menuMgr.add(collapseAllAction); IActionBars actionBars = viewSite.getActionBars(); actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), copyAction); actionBars.updateActionBars(); } /** * Handles the context menu showing. * * @param manager context menu manager */ private void handleMenuAboutToShow(IMenuManager manager) { IStructuredSelection selection = (IStructuredSelection)treeViewer.getSelection(); boolean isRelaunchEnabledForSelection = !selection.isEmpty() && (testingSession.getTestsRunnerProviderInfo().isAllowedMultipleTestFilter() || (selection.size() == 1)); rerunAction.setEnabled(isRelaunchEnabledForSelection); rerunAction.setTestingSession(testingSession); redebugAction.setEnabled(isRelaunchEnabledForSelection); redebugAction.setTestingSession(testingSession); copyAction.setEnabled(!selection.isEmpty()); boolean hasAnything = treeViewer.getInput() != null; expandAllAction.setEnabled(hasAnything); collapseAllAction.setEnabled(hasAnything); } /** * Sets the testing session to show. * * @param testingSession testing session or null to set default empty * session */ public void setTestingSession(ITestingSession testingSession) { this.testingSession = testingSession; treeViewer.setInput(testingSession != null ? testingSession.getModelAccessor().getRootSuite() : null); } /** * Provides access to the main widget of the tests hierarchy viewer. * * @return main widget of the tests hierarchy viewer */ public TreeViewer getTreeViewer() { return treeViewer; } /** * Move the selection to the next failed test case. */ public void showNextFailure() { showFailure(true); } /** * Move the selection to the previous failed test case. */ public void showPreviousFailure() { showFailure(false); } /** * Common implementation for movement the selection to the next or previous * failed test case. * * @param next true if the next failed test case should be selected and false otherwise */ private void showFailure(boolean next) { IStructuredSelection selection = (IStructuredSelection) getTreeViewer().getSelection(); ITestItem selected = (ITestItem) selection.getFirstElement(); ITestItem failedItem; if (selected == null) { ITestItem rootSuite = (ITestItem)treeViewer.getInput(); // For next element we should also check its children, for previous shouldn't. failedItem = findFailedImpl(rootSuite, null, next, next); } else { // For next element we should also check its children, for previous shouldn't. failedItem = findFailedImpl(selected.getParent(), selected, next, next); } if (failedItem != null) getTreeViewer().setSelection(new StructuredSelection(failedItem), true); } /** * Returns the next or previous failed test case relatively to the * <code>currItem</code> that should be a child of <code>parentItem</code>. * If the such item was not found through the children, it steps up to the * parent and continues search. * * @param parentItem parent test item to the current one * @param currItem current item search should be started from or null if * there is no any * @param next true if the next failed test case should be looked for and * false otherwise * @param checkCurrentChild specifies whether the search should be also * through the children for the current item * @return found item or null */ private ITestItem findFailedImpl(ITestItem parentItem, ITestItem currItem, boolean next, boolean checkCurrentChild) { ITestItem result = findFailedChild(parentItem, currItem, next, checkCurrentChild); if (result != null) { return result; } // Nothing found at this level - try to step up ITestSuite grandParentItem = parentItem.getParent(); if (grandParentItem != null) { return findFailedImpl(grandParentItem, parentItem, next, false); } return null; } /** * Returns the next or previous failed test case relatively to the * <code>currItem</code> that should be a child of <code>parentItem</code>. * Note that unlike <code>findFailedImpl()</code> this method search only * through the children items. * * @param parentItem parent test item to the current one * @param currItem current item search should be started from or null if * there is no any * @param next true if the next failed test case should be looked for and * false otherwise * @param checkCurrentChild specifies whether the search should be also * through the children for the current item * @return found item or null */ private ITestItem findFailedChild(ITestItem parentItem, ITestItem currItem, boolean next, boolean checkCurrentChild) { ITestItem[] children = parentItem.getChildren(); boolean doSearch = (currItem == null); int increment = next ? 1 : -1; int startIndex = next ? 0 : children.length-1; int endIndex = next ? children.length : -1; for (int index = startIndex; index != endIndex; index += increment) { ITestItem item = children[index]; // Check element if (doSearch) { if (item instanceof ITestCase && item.getStatus().isError()) { return item; } } // If children of current element should be checked we should enable search here (if necessary) if (checkCurrentChild && item == currItem) { doSearch = true; } // Search element's children if (doSearch) { ITestItem result = findFailedChild(item, null, next, checkCurrentChild); if (result != null) { return result; } } // If children of current element should NOT be checked we should enable search here if (!checkCurrentChild && item == currItem) { doSearch = true; } } return null; } /** * Returns whether test items execution time should be shown in tests * hierarchy. * * @return true if time should be shown and false otherwise */ public boolean showTime() { return showTime; } /** * Sets whether test items execution time should be shown in tests * hierarchy. Updates tests hierarchy viewer if the view is changed. * * @param showTime true if time is shown and false otherwise */ public void setShowTime(boolean showTime) { if (this.showTime != showTime) { this.showTime = showTime; getTreeViewer().refresh(); } } /** * Sets whether only failed tests should be shown. * * @param showFailedOnly new filter state */ public void setShowFailedOnly(boolean showFailedOnly) { // Create filter on first demand if (failedOnlyFilter == null) { failedOnlyFilter = new FailedOnlyFilter(); } if (showFailedOnly) { getTreeViewer().addFilter(failedOnlyFilter); } else { getTreeViewer().removeFilter(failedOnlyFilter); } } /** * Returns whether tests hierarchy should be shown in flat or hierarchical * mode. * * @return tests hierarchy view mode */ public boolean showTestsHierarchy() { return showTestsHierarchy; } /** * Sets whether tests hierarchy should be shown in flat or hierarchical * mode. Updates tests hierarchy viewer if the view is changed. * * @param showTestsHierarchy true if tests hierarchy is shown in * hierarchical mode and false otherwise */ public void setShowTestsHierarchy(boolean showTestsHierarchy) { if (this.showTestsHierarchy != showTestsHierarchy) { this.showTestsHierarchy = showTestsHierarchy; getTreeViewer().refresh(); } } }