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