package org.testng.eclipse.ui;
import static org.testng.eclipse.ui.Images.IMG_TEST_HIERARCHY;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
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.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CLabel;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.custom.ViewForm;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.IMemento;
import org.testng.eclipse.TestNGPlugin;
import org.testng.eclipse.ui.tree.BaseTreeItem;
import org.testng.eclipse.ui.tree.ClassTreeItem;
import org.testng.eclipse.ui.tree.ITreeItem;
import org.testng.eclipse.ui.tree.SuiteTreeItem;
import org.testng.eclipse.ui.tree.TestMethodParametersTreeItem;
import org.testng.eclipse.ui.tree.TestMethodTreeItem;
import org.testng.eclipse.ui.tree.TestTreeItem;
import org.testng.eclipse.util.ResourceUtil;
import org.testng.eclipse.util.StringUtils;
/**
* This class is responsible for the tree display in the runner view part. It
* has two subclasses, SuccessTab and FailureTab. Whenever a new test result
* message arrives from RemoteTestNG, updateTestResult() is invoked, which then
* creates or updates the corresponding node in the tree.
*
* @author Cedric Beust <cedric@beust.com>
*/
abstract public class AbstractTab extends TestRunTab implements IMenuListener {
private Image m_testHierarchyIcon;
private Image m_stackViewIcon;
// Persistence tags
static final String TAG_RATIO = "ratio"; //$NON-NLS-1$
/** Used to persist the state of the layout */
private IMemento m_stateMemento;
/** The component that displays the stack trace when an item is selected */
private FailureTrace m_failureTraceComponent;
private Tree m_tree;
private TreeViewer m_treeViewer;
private TestRunnerViewPart m_testRunnerPart;
private SashForm m_sashForm;
private Composite m_parentComposite;
//
// Keeping track of what's in the tree
//
private Map<String, ITreeItem> m_treeItemMap = Maps.newHashMap();
private Set<RunInfo> m_runInfos = Sets.newHashSet();
private String m_searchFilter = "";
private Map<String, ITreeItem> m_suites = Maps.newHashMap();
private Map<String, ITreeItem> m_tests = Maps.newHashMap();
private Map<String, ITreeItem> m_classes = Maps.newHashMap();
private Map<String, ITreeItem> m_methods = Maps.newHashMap();
// Don't forget to .clear() all these maps beween each run ^^
@Override
public String getSelectedTestId() {
TreeItem[] treeItems = m_tree.getSelection();
if (treeItems == null || treeItems.length == 0) {
return null;
} else {
return BaseTreeItem.getTreeItem(treeItems[0]).getRunInfo().getMethodId();
}
}
@Override
public void setSelectedTest(String testId) {
if (testId == null) return;
ITreeItem node = m_treeItemMap.get(testId);
if (node != null) {
m_tree.select(node.getTreeItem());
} else {
m_tree.deselectAll();
}
}
@Override
public Image getImage() {
return m_testHierarchyIcon;
}
@Override
public Composite createTabControl(Composite parent, TestRunnerViewPart runner) {
m_testRunnerPart = runner;
Composite result = new Composite(parent, SWT.NONE);
GridLayout gridLayout = new GridLayout();
gridLayout.marginHeight = 0;
gridLayout.marginWidth = 0;
gridLayout.numColumns = 3;
result.setLayout(gridLayout);
GridData gridData = new GridData(GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL);
result.setLayoutData(gridData);
// The sash is the parent of both the tree and the stack trace component
m_sashForm = new SashForm(result, SWT.HORIZONTAL);
m_sashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
//
// Tree
//
m_tree = new Tree(m_sashForm, SWT.SINGLE | SWT.VIRTUAL);
// m_treeViewer = new TreeViewer(m_sashForm, SWT.SINGLE);
//
// Stack trace (FailureComponent)
//
ViewForm stackTraceForm = new ViewForm(m_sashForm, SWT.NONE);
stackTraceForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
CLabel label = new CLabel(stackTraceForm, SWT.NONE);
label.setText(ResourceUtil.getString("TestRunnerViewPart.label.failure")); //$NON-NLS-1$
label.setImage(m_stackViewIcon);
stackTraceForm.setTopLeft(label);
ToolBar failureToolBar = new ToolBar(stackTraceForm, SWT.FLAT | SWT.WRAP);
stackTraceForm.setTopCenter(failureToolBar);
m_failureTraceComponent = new FailureTrace(stackTraceForm, m_testRunnerPart, failureToolBar);
stackTraceForm.setContent(m_failureTraceComponent.getComposite());
m_sashForm.setWeights(new int[] { 50, 50 });
initImages();
initMenu();
addListeners();
m_parentComposite = result;
return result;
}
private void initMenu() {
MenuManager menuMgr = new MenuManager();
menuMgr.setRemoveAllWhenShown(true);
menuMgr.addMenuListener(this);
Menu menu = menuMgr.createContextMenu(m_tree);
m_tree.setMenu(menu);
}
private void addListeners() {
m_tree.addSelectionListener(new SelectionListener() {
public void widgetSelected(SelectionEvent e) {
activate();
}
public void widgetDefaultSelected(SelectionEvent e) {
activate();
}
});
m_tree.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent e) {
disposeIcons();
}
});
m_tree.addMouseListener(new MouseAdapter() {
@Override
public void mouseDoubleClick(MouseEvent e) {
handleDoubleClick(e);
}
});
}
void disposeIcons() {
// if (m_suiteIcon != null) m_suiteIcon.dispose();
// if (m_suiteOkeIcon != null) m_suiteOkeIcon.dispose();
// if (m_suiteFailIcon != null) m_suiteFailIcon.dispose();
// if (m_suiteSkipIcon != null) m_suiteSkipIcon.dispose();
// if (m_suiteRunIcon != null) m_suiteRunIcon.dispose();
//
// if (m_testHierarchyIcon != null) m_testHierarchyIcon.dispose();
// if (m_testIcon != null) m_testIcon.dispose();
// if (m_testOkeIcon != null) m_testOkeIcon.dispose();
// if (m_testFailIcon != null) m_testFailIcon.dispose();
// if (m_testSkipIcon != null) m_testSkipIcon.dispose();
// if (m_testRunIcon != null) m_testRunIcon.dispose();
//
// if (m_stackViewIcon != null) m_stackViewIcon.dispose();
}
void handleDoubleClick(MouseEvent e) {
ITreeItem testInfo = getSelectedTreeItem();
if (null == testInfo) {
return;
}
if (testInfo instanceof TestMethodTreeItem) {
OpenTestAction action = new OpenTestAction(m_testRunnerPart, testInfo.getRunInfo());
if(action.isEnabled()) {
action.run();
}
}
}
/**
* @return the RunInfo associated with the current selection, or null it not applicable.
*/
private ITreeItem getSelectedTreeItem() {
TreeItem[] treeItems= m_tree.getSelection();
return treeItems.length == 0 ? null
: BaseTreeItem.getTreeItem(treeItems[0]);
}
/**
* @see org.eclipse.jface.action.IMenuListener#menuAboutToShow(org.eclipse.jface.action.IMenuManager)
*/
public void menuAboutToShow(IMenuManager manager) {
if(m_tree.getSelectionCount() > 0) {
TreeItem treeItem = m_tree.getSelection()[0];
RunInfo testInfo = BaseTreeItem.getTreeItem(treeItem).getRunInfo();
manager.add(new OpenTestAction(m_testRunnerPart, testInfo));
manager.add(new Separator());
manager.add(new QuickRunAction(m_testRunnerPart.getLaunchedProject(),
m_testRunnerPart.getLastLaunch(),
testInfo,
ILaunchManager.RUN_MODE));
manager.add(new QuickRunAction(m_testRunnerPart.getLaunchedProject(),
m_testRunnerPart.getLastLaunch(),
testInfo,
ILaunchManager.DEBUG_MODE));
manager.add(new Separator());
manager.add(new ExpandAllAction());
}
}
/**
* This method is invoked whenever a new test result arrives. Before adding the
* corresponding method node, it makes sure that all its ancestors (class, test, suite)
* exist and create them if they don't. Then it adds the time of the new test method
* to all these nodes.
*/
@Override
public void updateTestResult(RunInfo runInfo, boolean expand) {
m_runInfos.add(runInfo);
if (acceptTestResult(runInfo) && matchesSearchFilter(runInfo)) {
ITreeItem suite = m_suites.get(runInfo.getSuiteName());
if (suite == null) {
suite = new SuiteTreeItem(m_tree, runInfo);
m_suites.put(runInfo.getSuiteName(), suite);
}
String pathToTest = runInfo.getSuiteName() + "#" + runInfo.getTestName();
ITreeItem test = m_tests.get(pathToTest);
if (test == null) {
test = new TestTreeItem(suite.getTreeItem(), runInfo);
m_tests.put(pathToTest, test);
}
String pathToClass = pathToTest + "#" + runInfo.getInstanceName();
ITreeItem cls = m_classes.get(pathToClass);
if (cls == null) {
cls = new ClassTreeItem(test.getTreeItem(), runInfo);
m_classes.put(pathToClass, cls);
}
String pathToMethod = pathToClass + "#" + runInfo.getMethodName();
ITreeItem method = m_methods.get(pathToMethod);
if (method == null) {
method = new TestMethodTreeItem(cls.getTreeItem(), runInfo);
m_methods.put(pathToMethod, method);
}
// Create a node for the parameter values, if applicable
ITreeItem methodParam = null;
if (!StringUtils.isEmptyString(runInfo.getParametersDisplay())) {
methodParam = new TestMethodParametersTreeItem(method.getTreeItem(),
runInfo);
methodParam.addToCumulatedTime(runInfo);
}
if (expand) {
suite.getTreeItem().setExpanded(true);
test.getTreeItem().setExpanded(true);
cls.getTreeItem().setExpanded(true);
method.getTreeItem().setExpanded(true);
if (methodParam != null) {
methodParam.getTreeItem().setExpanded(true);
focus(methodParam.getTreeItem());
} else {
focus(method.getTreeItem());
}
}
suite.addToCumulatedTime(runInfo);
test.addToCumulatedTime(runInfo);
cls.addToCumulatedTime(runInfo);
method.addToCumulatedTime(runInfo);
}
}
private void focus(TreeItem treeItem) {
// how to remove the annoying selection background?
treeItem.getParent().setSelection(treeItem);
}
@Override
public void updateTestResult(List<RunInfo> results) {
if (results.size() > 0) {
reset();
for (RunInfo ri : results) {
updateTestResult(ri, false /* don't expand, we'll do that at the end */);
}
}
postExpandAll();
}
private void reset() {
m_suites.clear();
m_tests.clear();
m_classes.clear();
m_methods.clear();
m_tree.removeAll();
}
@Override
public void updateSearchFilter(String text) {
if (text.equals(m_searchFilter)) return;
m_searchFilter = text;
reset();
m_treeItemMap.clear();
for (final RunInfo runInfo : m_runInfos) {
if (matchesSearchFilter(runInfo)) {
updateTestResult(runInfo, true /* expand */);
}
}
}
private boolean matchesSearchFilter(RunInfo runInfo) {
if ("".equals(m_searchFilter)) return true;
else {
return Pattern.matches(".*" + m_searchFilter.toLowerCase() + ".*",
runInfo.getMethodDisplay().toLowerCase());
}
}
/**
* Override in subclasses to filter out nodes.
*/
protected boolean acceptTestResult(RunInfo runInfo) {
return true;
}
private void p(String string) {
if (false) {
System.out.println("[AbstractTab] " + string);
}
}
@Override
public void aboutToStart() {
reset();
m_runInfos.clear();
m_tree.removeAll();
m_treeItemMap = new Hashtable<String, ITreeItem>();
m_failureTraceComponent.clear();
}
@Override
public void activate() {
testSelected();
}
@Override
public void setFocus() {
m_tree.setFocus();
}
/**
* Expand all the nodes in the tree.
*/
private class ExpandAllAction extends Action {
public ExpandAllAction() {
setText(ResourceUtil.getString("ExpandAllAction.text")); //$NON-NLS-1$
setToolTipText(ResourceUtil.getString("ExpandAllAction.tooltip")); //$NON-NLS-1$
}
@Override
public void run() {
expandAll();
}
}
private void expandAll() {
m_tree.setRedraw(false);
for (TreeItem treeItem : m_tree.getItems()) {
expandAll(treeItem);
}
m_tree.setRedraw(true);
}
private void expandAll(TreeItem item) {
item.setExpanded(true);
for (TreeItem subItem : item.getItems()) {
expandAll(subItem);
}
}
private RunInfo getSelectedRunInfo() {
TreeItem[] treeItems = m_tree.getSelection();
if (treeItems.length == 0) {
return null;
} else {
return BaseTreeItem.getTreeItem(treeItems[0]).getRunInfo();
}
}
private void testSelected() {
postSyncRunnable(new Runnable() {
public void run() {
m_failureTraceComponent.showFailure(getSelectedRunInfo());
}
});
}
private void postSyncRunnable(Runnable r) {
m_tree.getDisplay().syncExec(r);
}
private void registerTreeItem(String id, ITreeItem treeItem) {
m_treeItemMap.put(id, treeItem);
}
/**
* Called after an item has been updated, meant to be overridden by subclasses
*/
protected void onPostUpdate(TreeItem ti, int state) {
}
private void postExpandAll() {
Runnable expandRunnable = new Runnable() {
public void run() {
m_tree.selectAll();
expandAll();
};
};
m_tree.getDisplay().syncExec(expandRunnable);
}
private void initImages() {
m_testHierarchyIcon = Images.getImage(IMG_TEST_HIERARCHY);
m_stackViewIcon = TestNGPlugin.getImageDescriptor("eview16/stackframe.gif")
.createImage(); //$NON-NLS-1$
}
private String getRatioTag() {
return getNameKey() + "." + TAG_RATIO;
}
@Override
public void saveState(IMemento memento) {
if(m_sashForm == null) {
// part has not been created
if(m_stateMemento != null) { //Keep the old state;
memento.putMemento(m_stateMemento);
}
return;
}
int[] weigths = m_sashForm.getWeights();
int ratio = (weigths[0] * 1000) / (weigths[0] + weigths[1]);
memento.putInteger(getRatioTag(), ratio);
}
@Override
public void restoreState(IMemento memento) {
if (memento == null) return;
Integer ratio = memento.getInteger(getRatioTag());
if (ratio != null) {
m_sashForm.setWeights(new int[] { ratio.intValue(), 1000 - ratio.intValue() });
}
}
@Override
public void setOrientation(boolean horizontal) {
m_sashForm.setOrientation(horizontal ? SWT.HORIZONTAL : SWT.VERTICAL);
}
// private void addResizeListener(Composite parent) {
// parent.addControlListener(new ControlListener() {
// public void controlMoved(ControlEvent e) {
// }
//
// public void controlResized(ControlEvent e) {
// computeOrientation();
// }
// });
// }
}