/******************************************************************************* * Copyright Technophobia Ltd 2012 * * This file is part of the Substeps Eclipse Plugin. * * The Substeps Eclipse Plugin is free software: you can redistribute it and/or modify * it under the terms of the Eclipse Public License v1.0. * * The Substeps Eclipse Plugin is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * Eclipse Public License for more details. * * You should have received a copy of the Eclipse Public License * along with the Substeps Eclipse Plugin. If not, see <http://www.eclipse.org/legal/epl-v10.html>. ******************************************************************************/ package com.technophobia.substeps.junit.ui.component; import java.util.AbstractList; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import org.eclipse.debug.core.ILaunchManager; 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.AbstractTreeViewer; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.StructuredViewer; import org.eclipse.jface.viewers.TableViewer; 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.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.TableItem; import org.eclipse.ui.IWorkbenchActionConstants; import org.eclipse.ui.IWorkbenchPartSite; import org.eclipse.ui.part.PageBook; import com.technophobia.eclipse.ui.Notifier; import com.technophobia.eclipse.ui.view.ViewLayout; import com.technophobia.substeps.junit.action.OpenFeatureAction; import com.technophobia.substeps.junit.action.RerunTestAction; import com.technophobia.substeps.junit.ui.SubstepsFeatureMessages; import com.technophobia.substeps.junit.ui.SubstepsIconProvider; import com.technophobia.substeps.junit.ui.SubstepsRunSession; import com.technophobia.substeps.junit.ui.TestContext; import com.technophobia.substeps.junit.ui.label.TestSessionLabelProvider; import com.technophobia.substeps.model.structure.Status; import com.technophobia.substeps.model.structure.SubstepsTestElement; import com.technophobia.substeps.model.structure.SubstepsTestLeafElement; import com.technophobia.substeps.model.structure.SubstepsTestParentElement; import com.technophobia.substeps.model.structure.SubstepsTestRootElement; import com.technophobia.substeps.supplier.Supplier; public class FeatureViewer { private final class TestSelectionListener implements ISelectionChangedListener { @Override public void selectionChanged(final SelectionChangedEvent event) { handleSelected(); } } private final class TestOpenListener extends SelectionAdapter { @Override public void widgetDefaultSelected(final SelectionEvent e) { handleDefaultSelected(); } } private final class FailuresOnlyFilter extends ViewerFilter { @Override public boolean select(final Viewer viewer, final Object parentElement, final Object element) { return select(((SubstepsTestElement) element)); } public boolean select(final SubstepsTestElement testElement) { final Status status = testElement.getStatus(); if (status.isErrorOrFailure()) return true; return !testRunSession.isRunning() && (status.equals(Status.RUNNING)); // rerunning } } private static class ReverseList<E> extends AbstractList<E> { private final List<E> list; public ReverseList(final List<E> list) { this.list = list; } @Override public E get(final int index) { return list.get(list.size() - index - 1); } @Override public int size() { return list.size(); } } private class ExpandAllAction extends Action { public ExpandAllAction() { setText(SubstepsFeatureMessages.ExpandAllAction_text); setToolTipText(SubstepsFeatureMessages.ExpandAllAction_tooltip); } @Override public void run() { treeViewer.expandAll(); } } private final FailuresOnlyFilter failuresOnlyFilter = new FailuresOnlyFilter(); private PageBook viewerbook; private TreeViewer treeViewer; private TestSessionTreeContentProvider treeContentProvider; private TestSessionLabelProvider treeLabelProvider; private TableViewer tableViewer; private TestSessionTableContentProvider tableContentProvider; private TestSessionLabelProvider tableLabelProvider; private SelectionProviderMediator selectionProvider; private ViewLayout layoutMode; private boolean treeHasFilter; private boolean tableHasFilter; private SubstepsRunSession testRunSession; private boolean treeNeedsRefresh; private boolean tableNeedsRefresh; private HashSet<SubstepsTestElement> needUpdate; private SubstepsTestLeafElement autoScrollTarget; private LinkedList<SubstepsTestParentElement> autoClose; private HashSet<SubstepsTestParentElement> autoExpand; private final Supplier<IWorkbenchPartSite> siteSupplier; private final Notifier<Boolean> autoScrollSupplier; private final Notifier<SubstepsTestElement> failedTestNotifier; private final Supplier<String> testKindDisplayNameSupplier; private final SubstepsIconProvider iconProvider; private final Notifier<TestContext> testRunner; public FeatureViewer(final Composite parent, final Supplier<IWorkbenchPartSite> siteSupplier, final Notifier<SubstepsTestElement> failedTestNotifier, final Notifier<TestContext> testRunner, final Notifier<Boolean> autoScrollSupplier, final Supplier<String> testKindDisplayNameSupplier, final SubstepsIconProvider iconProvider) { this.siteSupplier = siteSupplier; this.failedTestNotifier = failedTestNotifier; this.testRunner = testRunner; this.autoScrollSupplier = autoScrollSupplier; this.testKindDisplayNameSupplier = testKindDisplayNameSupplier; this.iconProvider = iconProvider; this.layoutMode = ViewLayout.HIERARCHICAL; createTestViewers(parent); registerViewersRefresh(); initContextMenu(); } private void createTestViewers(final Composite parent) { viewerbook = new PageBook(parent, SWT.NULL); treeViewer = new TreeViewer(viewerbook, SWT.V_SCROLL | SWT.SINGLE); treeViewer.setUseHashlookup(true); treeContentProvider = new TestSessionTreeContentProvider(); treeViewer.setContentProvider(treeContentProvider); treeLabelProvider = new TestSessionLabelProvider(testKindDisplayNameSupplier, iconProvider, ViewLayout.HIERARCHICAL); treeViewer.setLabelProvider(new ColoringLabelProvider(treeLabelProvider)); tableViewer = new TableViewer(viewerbook, SWT.V_SCROLL | SWT.H_SCROLL | SWT.SINGLE); tableViewer.setUseHashlookup(true); tableContentProvider = new TestSessionTableContentProvider(); tableViewer.setContentProvider(tableContentProvider); tableLabelProvider = new TestSessionLabelProvider(testKindDisplayNameSupplier, iconProvider, ViewLayout.FLAT); tableViewer.setLabelProvider(new ColoringLabelProvider(tableLabelProvider)); selectionProvider = new SelectionProviderMediator(new StructuredViewer[] { treeViewer, tableViewer }, treeViewer); selectionProvider.addSelectionChangedListener(new TestSelectionListener()); final TestOpenListener testOpenListener = new TestOpenListener(); treeViewer.getTree().addSelectionListener(testOpenListener); tableViewer.getTable().addSelectionListener(testOpenListener); siteSupplier.get().setSelectionProvider(selectionProvider); viewerbook.showPage(treeViewer.getTree()); } private void initContextMenu() { final MenuManager menuMgr = new MenuManager("#PopupMenu"); //$NON-NLS-1$ menuMgr.setRemoveAllWhenShown(true); menuMgr.addMenuListener(new IMenuListener() { @Override public void menuAboutToShow(final IMenuManager manager) { handleMenuAboutToShow(manager); } }); siteSupplier.get().registerContextMenu(menuMgr, selectionProvider); final Menu menu = menuMgr.createContextMenu(viewerbook); treeViewer.getTree().setMenu(menu); tableViewer.getTable().setMenu(menu); } void handleMenuAboutToShow(final IMenuManager manager) { final IStructuredSelection selection = (IStructuredSelection) selectionProvider.getSelection(); if (!selection.isEmpty()) { final SubstepsTestElement testElement = (SubstepsTestElement) selection.getFirstElement(); final String className = testElement.getClassName(); if (testElement instanceof SubstepsTestParentElement) { manager.add(new OpenFeatureAction(testElement)); manager.add(new Separator()); if (testClassExists(className) && !testRunSession.isKeptAlive()) { manager.add(new RerunTestAction(SubstepsFeatureMessages.RerunAction_label_run, testRunner, testElement.getId(), className, null, ILaunchManager.RUN_MODE)); manager.add(new RerunTestAction(SubstepsFeatureMessages.RerunAction_label_debug, testRunner, testElement.getId(), className, null, ILaunchManager.DEBUG_MODE)); } } else { final SubstepsTestLeafElement testCaseElement = (SubstepsTestLeafElement) testElement; final String testMethodName = testCaseElement.getTestMethodName(); manager.add(new OpenFeatureAction(testElement)); manager.add(new Separator()); if (testRunSession.isKeptAlive()) { manager.add(new RerunTestAction(SubstepsFeatureMessages.RerunAction_label_rerun, testRunner, testElement.getId(), className, testMethodName, ILaunchManager.RUN_MODE)); } else { manager.add(new RerunTestAction(SubstepsFeatureMessages.RerunAction_label_run, testRunner, testElement.getId(), className, testMethodName, ILaunchManager.RUN_MODE)); manager.add(new RerunTestAction(SubstepsFeatureMessages.RerunAction_label_debug, testRunner, testElement.getId(), className, testMethodName, ILaunchManager.DEBUG_MODE)); } } if (layoutMode.equals(ViewLayout.HIERARCHICAL)) { manager.add(new Separator()); manager.add(new ExpandAllAction()); } } // if (testRunSession != null && testRunSession.getFailureCount() + // testRunSession.getErrorCount() > 0) { // if (!layoutMode.equals(ViewLayout.HIERARCHICAL)) // manager.add(new Separator()); // manager.add(new CopyFailureListAction(testRunnerPart, clipboard)); // } manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS)); manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS + "-end")); //$NON-NLS-1$ } private boolean testClassExists(@SuppressWarnings("unused") final String className) { throw new UnsupportedOperationException("This is not implemented - needs to be features, not classes"); // final IJavaProject project = testRunnerPart.getLaunchedProject(); // if (project == null) // return false; // try { // final IType type = project.findType(className); // return type != null; // } catch (final JavaModelException e) { // // fall through // } // return false; } public Control getTestViewerControl() { return viewerbook; } public synchronized void registerActiveSession(final SubstepsRunSession session) { this.testRunSession = session; registerAutoScrollTarget(null); registerViewersRefresh(); } void handleDefaultSelected() { final IStructuredSelection selection = (IStructuredSelection) selectionProvider.getSelection(); if (selection.size() != 1) return; final SubstepsTestElement testElement = (SubstepsTestElement) selection.getFirstElement(); OpenFeatureAction action; if (testElement instanceof SubstepsTestParentElement) { action = new OpenFeatureAction(testElement); } else if (testElement instanceof SubstepsTestLeafElement) { final SubstepsTestLeafElement testCase = (SubstepsTestLeafElement) testElement; action = new OpenFeatureAction(testCase); } else { throw new IllegalStateException(String.valueOf(testElement)); } if (action.isEnabled()) action.run(); } private void handleSelected() { final IStructuredSelection selection = (IStructuredSelection) selectionProvider.getSelection(); SubstepsTestElement testElement = null; if (selection.size() == 1) { testElement = (SubstepsTestElement) selection.getFirstElement(); } failedTestNotifier.notify(testElement); } public synchronized void setShowTime(final boolean showTime) { try { viewerbook.setRedraw(false); treeLabelProvider.setShowTime(showTime); tableLabelProvider.setShowTime(showTime); } finally { viewerbook.setRedraw(true); } } public synchronized void setShowFailuresOnly(final boolean failuresOnly, final int layoutMode) { /* * Management of fTreeViewer and fTableViewer * ****************************************** - invisible viewer is * updated on registerViewerUpdate unless its f*NeedsRefresh is true - * invisible viewer is not refreshed upfront - on layout change, new * viewer is refreshed if necessary - filter only applies to "current" * layout mode / viewer */ try { viewerbook.setRedraw(false); IStructuredSelection selection = null; final boolean switchLayout = layoutMode != this.layoutMode.value(); if (switchLayout) { selection = (IStructuredSelection) selectionProvider.getSelection(); if (layoutMode == ViewLayout.HIERARCHICAL.value()) { if (treeNeedsRefresh) { clearUpdateAndExpansion(); } } else { if (tableNeedsRefresh) { clearUpdateAndExpansion(); } } this.layoutMode = ViewLayout.forValue(layoutMode); viewerbook.showPage(getActiveViewer().getControl()); } // avoid realizing all TableItems, especially in flat mode! final StructuredViewer viewer = getActiveViewer(); if (failuresOnly) { if (!getActiveViewerHasFilter()) { setActiveViewerNeedsRefresh(true); setActiveViewerHasFilter(true); viewer.setInput(null); viewer.addFilter(failuresOnlyFilter); } } else { if (getActiveViewerHasFilter()) { setActiveViewerNeedsRefresh(true); setActiveViewerHasFilter(false); viewer.setInput(null); viewer.removeFilter(failuresOnlyFilter); } } processChangesInUI(); if (selection != null) { // workaround for // https://bugs.eclipse.org/bugs/show_bug.cgi?id=125708 // (ITreeSelection not adapted if TreePaths changed): final StructuredSelection flatSelection = new StructuredSelection(selection.toList()); selectionProvider.setSelection(flatSelection, true); } } finally { viewerbook.setRedraw(true); } } private boolean getActiveViewerHasFilter() { if (layoutMode.equals(ViewLayout.HIERARCHICAL)) return treeHasFilter; return tableHasFilter; } private void setActiveViewerHasFilter(final boolean filter) { if (layoutMode.equals(ViewLayout.HIERARCHICAL)) treeHasFilter = filter; else tableHasFilter = filter; } private StructuredViewer getActiveViewer() { if (layoutMode.equals(ViewLayout.HIERARCHICAL)) return treeViewer; return tableViewer; } private boolean getActiveViewerNeedsRefresh() { if (layoutMode.equals(ViewLayout.HIERARCHICAL)) return treeNeedsRefresh; return tableNeedsRefresh; } private void setActiveViewerNeedsRefresh(final boolean needsRefresh) { if (layoutMode.equals(ViewLayout.HIERARCHICAL)) treeNeedsRefresh = needsRefresh; else tableNeedsRefresh = needsRefresh; } /** * To be called periodically by the TestRunnerViewPart (in the UI thread). */ public void processChangesInUI() { SubstepsTestRootElement testRoot; if (testRunSession == null) { registerViewersRefresh(); treeNeedsRefresh = false; tableNeedsRefresh = false; treeViewer.setInput(null); tableViewer.setInput(null); return; } testRoot = testRunSession.getTestRoot(); final StructuredViewer viewer = getActiveViewer(); if (getActiveViewerNeedsRefresh()) { clearUpdateAndExpansion(); setActiveViewerNeedsRefresh(false); viewer.setInput(testRoot); } else { Object[] toUpdate; synchronized (this) { toUpdate = needUpdate.toArray(); needUpdate.clear(); } if (!treeNeedsRefresh && toUpdate.length > 0) { if (treeHasFilter) for (final Object element : toUpdate) updateElementInTree((SubstepsTestElement) element); else { final HashSet<Object> toUpdateWithParents = new HashSet<Object>(); toUpdateWithParents.addAll(Arrays.asList(toUpdate)); for (final Object element : toUpdate) { SubstepsTestElement parent = ((SubstepsTestElement) element).getParent(); while (parent != null) { toUpdateWithParents.add(parent); parent = parent.getParent(); } } treeViewer.update(toUpdateWithParents.toArray(), null); } } if (!tableNeedsRefresh && toUpdate.length > 0) { if (tableHasFilter) for (final Object element : toUpdate) updateElementInTable((SubstepsTestElement) element); else tableViewer.update(toUpdate, null); } } autoScrollInUI(); } private void updateElementInTree(final SubstepsTestElement testElement) { if (isShown(testElement)) { updateShownElementInTree(testElement); } else { SubstepsTestElement current = testElement; do { if (treeViewer.testFindItem(current) != null) treeViewer.remove(current); current = current.getParent(); } while (!(current instanceof SubstepsTestRootElement) && !isShown(current)); while (current != null && !(current instanceof SubstepsTestRootElement)) { treeViewer.update(current, null); current = current.getParent(); } } } private void updateShownElementInTree(final SubstepsTestElement testElement) { if (testElement == null || testElement instanceof SubstepsTestRootElement) // paranoia // null // check return; final SubstepsTestParentElement parent = testElement.getParent(); updateShownElementInTree(parent); // make sure parent is shown and // up-to-date if (treeViewer.testFindItem(testElement) == null) { treeViewer.add(parent, testElement); // if not yet in tree: add } else { treeViewer.update(testElement, null); // if in tree: update } } private void updateElementInTable(final SubstepsTestElement element) { if (isShown(element)) { if (tableViewer.testFindItem(element) == null) { final SubstepsTestElement previous = getNextFailure(element, false); int insertionIndex = -1; if (previous != null) { final TableItem item = (TableItem) tableViewer.testFindItem(previous); if (item != null) insertionIndex = tableViewer.getTable().indexOf(item); } tableViewer.insert(element, insertionIndex); } else { tableViewer.update(element, null); } } else { tableViewer.remove(element); } } private boolean isShown(final SubstepsTestElement current) { return failuresOnlyFilter.select(current); } private void autoScrollInUI() { if (!autoScrollSupplier.currentValue().booleanValue()) { clearAutoExpand(); autoClose.clear(); return; } if (layoutMode.equals(ViewLayout.FLAT)) { if (autoScrollTarget != null) tableViewer.reveal(autoScrollTarget); return; } synchronized (this) { for (final SubstepsTestParentElement suite : autoExpand) { treeViewer.setExpandedState(suite, true); } clearAutoExpand(); } final SubstepsTestLeafElement current = autoScrollTarget; autoScrollTarget = null; SubstepsTestParentElement parent = current == null ? null : (SubstepsTestParentElement) treeContentProvider .getParent(current); if (autoClose.isEmpty() || !autoClose.getLast().equals(parent)) { // we're in a new branch, so let's close old OK branches: for (final ListIterator<SubstepsTestParentElement> iter = autoClose.listIterator(autoClose.size()); iter .hasPrevious();) { final SubstepsTestParentElement previousAutoOpened = iter.previous(); if (previousAutoOpened.equals(parent)) break; if (previousAutoOpened.getStatus().equals(Status.OK)) { // auto-opened the element, and all children are OK -> auto // close iter.remove(); treeViewer.collapseToLevel(previousAutoOpened, AbstractTreeViewer.ALL_LEVELS); } } while (parent != null && !testRunSession.getTestRoot().equals(parent) && treeViewer.getExpandedState(parent) == false) { autoClose.add(parent); // add to auto-opened elements -> close // later if STATUS_OK parent = (SubstepsTestParentElement) treeContentProvider.getParent(parent); } } if (current != null) treeViewer.reveal(current); } public void selectFirstFailure() { final SubstepsTestLeafElement firstFailure = getNextChildFailure(testRunSession.getTestRoot(), true); if (firstFailure != null) getActiveViewer().setSelection(new StructuredSelection(firstFailure), true); } public void selectFailure(final boolean showNext) { final IStructuredSelection selection = (IStructuredSelection) getActiveViewer().getSelection(); final SubstepsTestElement selected = (SubstepsTestElement) selection.getFirstElement(); SubstepsTestElement next; if (selected == null) { next = getNextChildFailure(testRunSession.getTestRoot(), showNext); } else { next = getNextFailure(selected, showNext); } if (next != null) getActiveViewer().setSelection(new StructuredSelection(next), true); } private SubstepsTestElement getNextFailure(final SubstepsTestElement selected, final boolean showNext) { if (selected instanceof SubstepsTestParentElement) { final SubstepsTestElement nextChild = getNextChildFailure((SubstepsTestParentElement) selected, showNext); if (nextChild != null) return nextChild; } return getNextFailureSibling(selected, showNext); } private SubstepsTestLeafElement getNextFailureSibling(final SubstepsTestElement current, final boolean showNext) { final SubstepsTestParentElement parent = current.getParent(); if (parent == null) return null; List<SubstepsTestElement> siblings = Arrays.asList(parent.getChildren()); if (!showNext) siblings = new ReverseList<SubstepsTestElement>(siblings); final int nextIndex = siblings.indexOf(current) + 1; for (int i = nextIndex; i < siblings.size(); i++) { final SubstepsTestElement sibling = siblings.get(i); if (sibling.getStatus().isErrorOrFailure()) { if (sibling instanceof SubstepsTestLeafElement) { return (SubstepsTestLeafElement) sibling; } return getNextChildFailure((SubstepsTestParentElement) sibling, showNext); } } return getNextFailureSibling(parent, showNext); } private SubstepsTestLeafElement getNextChildFailure(final SubstepsTestParentElement root, final boolean showNext) { List<SubstepsTestElement> children = Arrays.asList(root.getChildren()); if (!showNext) children = new ReverseList<SubstepsTestElement>(children); for (int i = 0; i < children.size(); i++) { final SubstepsTestElement child = children.get(i); if (child.getStatus().isErrorOrFailure()) { if (child instanceof SubstepsTestLeafElement) { return (SubstepsTestLeafElement) child; } return getNextChildFailure((SubstepsTestParentElement) child, showNext); } } return null; } public synchronized void registerViewersRefresh() { treeNeedsRefresh = true; tableNeedsRefresh = true; clearUpdateAndExpansion(); } private void clearUpdateAndExpansion() { needUpdate = new LinkedHashSet<SubstepsTestElement>(); autoClose = new LinkedList<SubstepsTestParentElement>(); autoExpand = new HashSet<SubstepsTestParentElement>(); } /** * @param testElement * the added test */ public synchronized void registerTestAdded(final SubstepsTestElement testElement) { // TODO: performance: would only need to refresh parent of added element treeNeedsRefresh = true; tableNeedsRefresh = true; } public synchronized void registerViewerUpdate(final SubstepsTestElement testElement) { needUpdate.add(testElement); } private synchronized void clearAutoExpand() { autoExpand.clear(); } public void registerAutoScrollTarget(final SubstepsTestLeafElement testCaseElement) { autoScrollTarget = testCaseElement; } public synchronized void registerFailedForAutoScroll(final SubstepsTestElement testElement) { final SubstepsTestParentElement parent = (SubstepsTestParentElement) treeContentProvider.getParent(testElement); if (parent != null) autoExpand.add(parent); } public void expandFirstLevel() { treeViewer.expandToLevel(2); } }