/**
* Copyright (c) 2005-2013 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Eclipse Public License (EPL).
* Please see the license.txt included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
package org.python.pydev.debug.pyunit;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.ui.IDebugUIConstants;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.JFaceColors;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DefaultInformationControl.IInformationPresenter;
import org.eclipse.jface.text.TextAttribute;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.swt.widgets.Widget;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.console.IHyperlink;
import org.python.pydev.core.log.Log;
import org.python.pydev.debug.core.PydevDebugPlugin;
import org.python.pydev.debug.newconsole.prefs.ColorManager;
import org.python.pydev.debug.pyunit.PyUnitViewTestsHolder.DummyPyUnitServer;
import org.python.pydev.debug.ui.ILinkContainer;
import org.python.pydev.debug.ui.PythonConsoleLineTracker;
import org.python.pydev.plugin.PydevPlugin;
import org.python.pydev.plugin.preferences.PydevPrefs;
import org.python.pydev.shared_core.SharedCorePlugin;
import org.python.pydev.shared_core.callbacks.ICallbackWithListeners;
import org.python.pydev.shared_core.string.FastStringBuffer;
import org.python.pydev.shared_interactive_console.console.ui.internal.ClipboardHandler;
import org.python.pydev.shared_ui.EditorUtils;
import org.python.pydev.shared_ui.tooltips.presenter.StyleRangeWithCustomData;
import org.python.pydev.shared_ui.tooltips.presenter.ToolTipPresenterHandler;
import org.python.pydev.shared_ui.utils.IViewWithControls;
import org.python.pydev.shared_ui.utils.RunInUiThread;
import org.python.pydev.shared_ui.utils.UIUtils;
import org.python.pydev.ui.ColorAndStyleCache;
import org.python.pydev.ui.NotifyViewCreated;
import org.python.pydev.ui.ViewPartWithOrientation;
/**
* ViewPart that'll listen to the PyUnitServer and show what's happening (with a green/red bar).
*
* Features:
*
* - Red/green bar -- OK
* - Relaunching the tests -- OK
* - Relaunching only the tests that failed -- OK
* - Show stack traces of errors (when selected) -- OK
* - Show output of test cases (when selected) -- OK
* - Show tests ran -- OK
* - Show only failed tests ran -- OK
* - Stop execution of the tests -- OK
* - Show the number of successes, failures and errors -- OK
* - Double-click to go to test -- OK
* - Show time of test (and allow reordering based on it) -- OK
* - Tooltip on hover for test with links -- OK
* - Use theme colors -- OK
* - Auto-show on test run should be an option. -- OK
* - Check: if it's created after a test suite started running, the results should be properly shown. -- OK
* - Allow the user to select test runner (Initially at least default and nose. Step 2: py.test) -- OK
* - Don't show results in the unittest view (only show in the console): in this situation, don't even start the server or use xml-rpc. -- OK
* - Pydev default test runner distributed -- OK
* - Show current test(s) being run (handle parallel execution) -- OK
* - Select some tests and make a new run with them. -- OK
* - Show total time to run tests. -- OK
* - Rerun tests on file changes -- OK
*
*
* Nice to have:
* - Hide or show output pane
* - If a string was different, show an improved diff (as JDT)
* - Save column order (tree.setColumnOrder(order))
* - Hide columns
* - Theming bug: when columns order change, the selected text for the last columns is not appearing
*
*
* References:
*
* http://www.eclipse.org/swt/snippets/
*
* Notes on tree/table: http://www.eclipse.org/swt/R3_2/new_and_noteworthy.html (see links below)
*
* Working: Sort table by column (applicable to tree: http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet2.java?view=markup&content-type=text%2Fvnd.viewcvs-markup&revision=HEAD )
* Working: Reorder columns by drag ( http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet193.java?view=markup&content-type=text%2Fvnd.viewcvs-markup&revision=HEAD )
* Working: Sort indicator in column header ( http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet192.java?view=markup&content-type=text%2Fvnd.viewcvs-markup&revision=HEAD )
*
* Based on org.eclipse.jdt.internal.junit.ui.TestRunnerViewPart (but it's really not meant to be reused)
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public class PyUnitView extends ViewPartWithOrientation implements IViewWithControls {
public static final String PYUNIT_VIEW_ORIENTATION = "PYUNIT_VIEW_ORIENTATION";
@Override
public String getOrientationPreferencesKey() {
return PYUNIT_VIEW_ORIENTATION;
}
public static final String PY_UNIT_TEST_RESULT = "RESULT";
private static final String PY_UNIT_VIEW_ID = "org.python.pydev.debug.pyunit.pyUnitView";
public static final String PYUNIT_VIEW_SHOW_ONLY_ERRORS = "PYUNIT_VIEW_SHOW_ONLY_ERRORS";
public static final boolean PYUNIT_VIEW_DEFAULT_SHOW_ONLY_ERRORS = true;
public static final String PYUNIT_VIEW_SHOW_VIEW_ON_TEST_RUN = "PYUNIT_VIEW_SHOW_VIEW_ON_TEST_RUN";
public static final boolean PYUNIT_VIEW_DEFAULT_SHOW_VIEW_ON_TEST_RUN = true;
public static final String PYUNIT_VIEW_BACKGROUND_RELAUNCH_SHOW_ONLY_ERRORS = "PYUNIT_VIEW_BACKGROUND_RELAUNCH_SHOW_ONLY_ERRORS";
public static final boolean PYUNIT_VIEW_DEFAULT_BACKGROUND_RELAUNCH_SHOW_ONLY_ERRORS = false;
public static int MAX_RUNS_TO_KEEP = PyUnitViewTestsHolder.MAX_RUNS_TO_KEEP;
private PyUnitTestRun currentRun;
private final PythonConsoleLineTracker lineTracker = new PythonConsoleLineTracker();
private final ActivateLinkmouseListener activateLinkmouseListener = new ActivateLinkmouseListener();
/*default*/static final int COL_INDEX = 0;
/*default*/static final int COL_RESULT = 1;
/*default*/static final int COL_TEST = 2;
/*default*/static final int COL_FILENAME = 3;
/*default*/static final int COL_TIME = 4;
/*default*/static final int NUMBER_OF_COLUMNS = 5;
private static final NumberFormatException NUMBER_FORMAT_EXCEPTION = new NumberFormatException();
/*default*/PythonConsoleLineTracker getLineTracker() {
return lineTracker;
}
private ColorAndStyleCache colorAndStyleCache;
private SashForm sash;
private Tree tree;
private StyledText testOutputText;
private CounterPanel fCounterPanel;
private PyUnitProgressBar fProgressBar;
private Label fStatus;
private Composite fCounterComposite;
private IPropertyChangeListener prefListener;
private PinHistoryAction fPinHistory = new PinHistoryAction(this);
/**
* Whether we should show only errors or not.
*/
private boolean showOnlyErrors;
/*default*/TreeColumn colIndex;
/*default*/TreeColumn colResult;
/*default*/TreeColumn colTest;
/*default*/TreeColumn colFile;
/*default*/TreeColumn colTime;
/**
* Have we disposed of the view?
*/
private boolean disposed = false;
public PyUnitView() {
this.currentRun = PyUnitViewTestsHolder.getCurrentTest();
if (SharedCorePlugin.inTestMode()) {
// leave showOnlyErrors at default under test
} else {
PydevDebugPlugin plugin = PydevDebugPlugin.getDefault();
IPreferenceStore preferenceStore = plugin.getPreferenceStore();
this.showOnlyErrors = preferenceStore.getBoolean(PYUNIT_VIEW_SHOW_ONLY_ERRORS);
}
NotifyViewCreated.notifyViewCreated(this);
lineTracker.init(new ILinkContainer() {
@Override
public void addLink(IHyperlink link, int offset, int length) {
if (testOutputText == null) {
return;
}
StyleRangeWithCustomData range = new StyleRangeWithCustomData();
range.underline = true;
try {
range.underlineStyle = SWT.UNDERLINE_LINK;
} catch (Throwable e) {
//Ignore (not available on earlier versions of eclipse)
}
//Set the proper color if it's available.
TextAttribute textAttribute = ColorManager.getDefault().getHyperlinkTextAttribute();
if (textAttribute != null) {
range.foreground = textAttribute.getForeground();
} else {
range.foreground = JFaceColors.getHyperlinkText(Display.getDefault());
}
range.start = offset;
range.length = length + 1;
range.customData = link;
testOutputText.setStyleRange(range);
}
@Override
public String getContents(int lineOffset, int lineLength) throws BadLocationException {
if (testOutputText == null) {
return "";
}
if (lineLength <= 0) {
return "";
}
try {
return testOutputText.getText(lineOffset, lineOffset + lineLength);
} catch (IllegalArgumentException e) {
return ""; //thrown on invalid range.
}
}
});
}
public PyUnitProgressBar getProgressBar() {
return fProgressBar;
}
public CounterPanel getCounterPanel() {
return fCounterPanel;
}
public Tree getTree() {
return tree;
}
@Override
public void createPartControl(Composite parent) {
Assert.isTrue(!disposed);
super.createPartControl(parent);
IInformationPresenter presenter = new InformationPresenterWithLineTracker();
final ToolTipPresenterHandler tooltip = new ToolTipPresenterHandler(parent.getShell(), presenter);
GridLayout layout = new GridLayout();
layout.numColumns = 1;
layout.verticalSpacing = 2;
layout.marginWidth = 0;
layout.marginHeight = 2;
parent.setLayout(layout);
configureToolBar();
fCounterComposite = new Composite(parent, SWT.NONE);
layout = new GridLayout();
fCounterComposite.setLayout(layout);
fCounterComposite.setLayoutData(new GridData(GridData.GRAB_HORIZONTAL | GridData.HORIZONTAL_ALIGN_FILL));
fCounterPanel = new CounterPanel(fCounterComposite);
fCounterPanel.setLayoutData(new GridData(GridData.GRAB_HORIZONTAL | GridData.HORIZONTAL_ALIGN_FILL));
fProgressBar = new PyUnitProgressBar(fCounterComposite);
fProgressBar.setLayoutData(new GridData(GridData.GRAB_HORIZONTAL | GridData.HORIZONTAL_ALIGN_FILL));
fStatus = new Label(fCounterComposite, 0);
GridData statusLayoutData = new GridData(GridData.GRAB_HORIZONTAL | GridData.HORIZONTAL_ALIGN_FILL);
statusLayoutData.grabExcessHorizontalSpace = true;
fStatus.setLayoutData(statusLayoutData);
fStatus.setText("Status");
sash = new SashForm(parent, SWT.HORIZONTAL);
GridData layoutData = new GridData();
layoutData.grabExcessHorizontalSpace = true;
layoutData.grabExcessVerticalSpace = true;
layoutData.horizontalAlignment = GridData.FILL;
layoutData.verticalAlignment = GridData.FILL;
sash.setLayoutData(layoutData);
tree = new Tree(sash, SWT.FULL_SELECTION | SWT.MULTI);
tooltip.install(tree);
tree.setHeaderVisible(true);
Listener sortListener = new PyUnitSortListener(this);
colIndex = createColumn(" ", 50, sortListener);
colResult = createColumn("Result", 70, sortListener);
colTest = createColumn("Test", 180, sortListener);
colFile = createColumn("File", 180, sortListener);
colTime = createColumn("Time (s)", 80, sortListener);
onControlCreated.call(tree);
tree.setSortColumn(colIndex);
tree.setSortDirection(SWT.DOWN);
tree.addMouseListener(new DoubleClickTreeItemMouseListener());
tree.addKeyListener(new EnterProssedTreeItemKeyListener());
tree.addSelectionListener(new SelectResultSelectionListener());
Menu menu = new Menu(tree.getShell(), SWT.POP_UP);
MenuItem runItem = new MenuItem(menu, SWT.PUSH);
runItem.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
relaunchSelectedTests(ILaunchManager.RUN_MODE);
}
});
runItem.setText("Run");
MenuItem debugItem = new MenuItem(menu, SWT.PUSH);
debugItem.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
relaunchSelectedTests(ILaunchManager.DEBUG_MODE);
}
});
debugItem.setText("Debug");
tree.setMenu(menu);
if (PydevPlugin.getDefault() != null) {
colorAndStyleCache = new ColorAndStyleCache(PydevPrefs.getChainedPrefStore());
prefListener = new IPropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent event) {
if (tree != null) {
String property = event.getProperty();
if (ColorAndStyleCache.isColorOrStyleProperty(property)) {
colorAndStyleCache.reloadProperty(property);
Color errorColor = getErrorColor();
TreeItem[] items = tree.getItems();
for (TreeItem item : items) {
PyUnitTestResult result = (PyUnitTestResult) item.getData(PY_UNIT_TEST_RESULT);
if (result != null) {
if (result.isOk()) {
} else if (result.isSkip()) {
} else {
//failure or error.
item.setForeground(errorColor);
}
}
}
if (fProgressBar != null) {
fProgressBar.updateErrorColor(true);
}
}
}
}
};
PydevPrefs.getChainedPrefStore().addPropertyChangeListener(prefListener);
}
StyledText text = new StyledText(sash, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL | SWT.READ_ONLY);
text.setFont(JFaceResources.getFont(IDebugUIConstants.PREF_CONSOLE_FONT));
this.setTextComponent(text);
onTestRunAdded();
}
private void configureToolBar() {
IActionBars actionBars = getViewSite().getActionBars();
IToolBarManager toolBar = actionBars.getToolBarManager();
IMenuManager menuManager = actionBars.getMenuManager();
ShowViewOnTestRunAction showViewOnTestRunAction = new ShowViewOnTestRunAction(this);
menuManager.add(showViewOnTestRunAction);
IAction showTestRunnerPreferencesAction = new ShowTestRunnerPreferencesAction(this);
menuManager.add(showTestRunnerPreferencesAction);
ShowOnlyFailuresAction action = new ShowOnlyFailuresAction(this);
toolBar.add(action);
action.setChecked(this.showOnlyErrors);
toolBar.add(new Separator());
toolBar.add(new RelaunchAction(this));
toolBar.add(new RelaunchErrorsAction(this));
toolBar.add(new StopAction(this));
toolBar.add(new Separator());
toolBar.add(new RelaunchInBackgroundAction(this));
toolBar.add(new Separator());
toolBar.add(new HistoryAction(this));
toolBar.add(fPinHistory);
toolBar.add(new RestorePinHistoryAction(this));
addOrientationPreferences(menuManager);
}
@Override
protected void setNewOrientation(int orientation) {
if (sash != null && !sash.isDisposed() && fCounterComposite != null && !fCounterComposite.isDisposed()) {
GridLayout layout = (GridLayout) fCounterComposite.getLayout();
if (orientation == VIEW_ORIENTATION_HORIZONTAL) {
sash.setOrientation(SWT.HORIZONTAL);
layout.numColumns = 2;
} else {
sash.setOrientation(SWT.VERTICAL);
layout.numColumns = 1;
}
fParent.layout();
}
}
private TreeColumn createColumn(String text, int width, Listener sortListener) {
TreeColumn col;
col = new TreeColumn(tree, SWT.LEFT);
col.setText(text);
col.setWidth(width);
col.setMoveable(true);
col.addListener(SWT.Selection, sortListener);
return col;
}
@Override
public void setFocus() {
this.tree.setFocus();
}
@Override
public void dispose() {
if (this.disposed) {
return;
}
this.disposed = true;
if (this.tree != null) {
Tree t = this.tree;
this.tree = null;
onControlDisposed.call(t);
t.dispose();
}
if (this.testOutputText != null) {
StyledText t = this.testOutputText;
this.testOutputText = null;
onControlDisposed.call(t);
t.dispose();
}
if (this.fCounterPanel != null) {
this.fCounterPanel.dispose();
this.fCounterPanel = null;
}
if (this.prefListener != null) {
PydevPrefs.getChainedPrefStore().removePropertyChangeListener(prefListener);
this.prefListener = null;
}
super.dispose();
}
public static PyUnitViewServerListener registerPyUnitServer(final IPyUnitServer pyUnitServer) {
return registerPyUnitServer(pyUnitServer, true);
}
/**
* Registers a pyunit server in the pyunit view (and vice versa), so, the server starts notifying the view
* about changes in it and the view makes the test run from the server the current one.
*/
public static PyUnitViewServerListener registerPyUnitServer(final IPyUnitServer pyUnitServer, boolean async) {
//We create a listener before and later set the view so that we don't run into racing condition errors!
final PyUnitViewServerListener serverListener = new PyUnitViewServerListener(pyUnitServer,
pyUnitServer.getPyUnitLaunch());
PyUnitViewTestsHolder.addServerListener(serverListener);
Runnable r = new Runnable() {
@Override
public void run() {
try {
PyUnitView view = getView();
if (view != null) {
view.onTestRunAdded();
}
} catch (Exception e) {
Log.log(e);
}
}
};
if (async) {
Display.getDefault().asyncExec(r);
} else {
Display.getDefault().syncExec(r);
}
return serverListener;
}
/**
* Must be called on the UI thread. Sets the view on all the server listeners (even if it was already set before
* as it may be that a view was closed and then a new one created).
*/
private void onTestRunAdded() {
synchronized (PyUnitViewTestsHolder.lockServerListeners) {
for (PyUnitViewServerListener listener : PyUnitViewTestsHolder.serverListeners) {
listener.setView(this); //Set in all, as it may be that they have an already disposed view registered.
}
if (PyUnitViewTestsHolder.serverListeners.size() > 0) {
//make the last one active.
this.setCurrentRun(PyUnitViewTestsHolder.serverListeners.getLast().getTestRun());
}
}
}
/**
* Gets the py unit view. May only be called in the UI thread. If the view is not visible, shows it if the
* preference to do that is set to true.
*
* Note that it may return null if the preference to show it is false and the view is not currently shown.
*/
public static PyUnitView getView() {
return (PyUnitView) UIUtils.getView(PY_UNIT_VIEW_ID, ShowViewOnTestRunAction.getShowViewOnTestRun());
}
/**
* Notifies that the test run has finished.
*/
/*default */void notifyFinished(PyUnitTestRun testRun) {
if (this.disposed) {
return;
}
// When a test finishes executing, save the configuration
if (testRun.savedDiskIndex == null) {
PyUnitViewTestsHolder.saveDiskIndexJob.schedule(20);
}
if (testRun != currentRun) {
return;
}
asyncUpdateCountersAndBar();
}
/**
* Notifies that a test result has been added.
*/
/*default*/void notifyTest(PyUnitTestResult result) {
if (this.disposed) {
return;
}
notifyTest(result, true);
}
/*default*/void notifyTestStarted(PyUnitTestStarted result) {
if (this.disposed) {
return;
}
if (result.getTestRun() != currentRun) {
return;
}
asyncUpdateCountersAndBar();
}
/**
* Used to update the number of tests available.
*/
/*default*/void notifyTestsCollected(PyUnitTestRun testRun) {
if (this.disposed) {
return;
}
if (testRun != currentRun) {
return;
}
asyncUpdateCountersAndBar();
}
/**
* Calls an update in the counters and progress bar asynchronously (in the UI thread).
*/
public void asyncUpdateCountersAndBar() {
RunInUiThread.async(new Runnable() {
@Override
public void run() {
updateCountersAndBar();
}
});
}
/**
* Called after a test has been run (so that we properly update the tree).
*/
private void notifyTest(PyUnitTestResult result, boolean updateBar) {
if (this.disposed) {
return;
}
if (result.getTestRun() != currentRun) {
return;
}
if (!showOnlyErrors || (showOnlyErrors && !result.isOk() && !result.isSkip())) {
TreeItem treeItem = new TreeItem(tree, 0);
File file = new File(result.location);
treeItem.setText(new String[] { result.index, result.status, result.test, file.getName(), result.time });
if (result.isOk()) {
} else if (result.isSkip()) {
} else {
// failure or error
Color errorColor = getErrorColor();
treeItem.setForeground(errorColor);
}
treeItem.setData(ToolTipPresenterHandler.TIP_DATA, result);
treeItem.setData(PY_UNIT_TEST_RESULT, result);
int selectionCount = tree.getSelectionCount();
if (selectionCount == 0) {
tree.setSelection(treeItem);
onSelectResult(result);
}
}
if (updateBar) {
updateCountersAndBar();
}
}
/**
* @return the color that should be used for errors.
*/
public Color getErrorColor() {
TextAttribute attribute = ColorManager.getDefault().getConsoleErrorTextAttribute();
if (attribute == null) {
return null;
}
Color errorColor = attribute.getForeground();
return errorColor;
}
/**
* @return the color that should be used for the foreground.
*/
public Color getForegroundColor() {
TextAttribute attribute = ColorManager.getDefault().getForegroundTextAttribute();
if (attribute == null) {
return null;
}
Color errorColor = attribute.getForeground();
return errorColor;
}
/**
* Updates the number of test runs and the bar with the current progress.
*/
private void updateCountersAndBar() {
if (fCounterPanel == null) {
return;
}
if (currentRun != null) {
String totalNumberOfRuns = currentRun.getTotalNumberOfRuns();
int numberOfRuns = currentRun.getNumberOfRuns();
int numberOfErrors = currentRun.getNumberOfErrors();
int numberOfFailures = currentRun.getNumberOfFailures();
try {
int totalAsInt;
if (currentRun.getFinished()) {
totalAsInt = numberOfRuns;
//Leave the number of runs what was set before!
// totalNumberOfRuns = Integer.toString(totalAsInt);
} else {
totalAsInt = Integer.parseInt(totalNumberOfRuns);
}
if (totalAsInt == 0 && numberOfRuns > 0) {
totalNumberOfRuns = "?";
throw NUMBER_FORMAT_EXCEPTION;
}
fProgressBar.reset(numberOfErrors + numberOfFailures > 0, false, numberOfRuns, totalAsInt);
} catch (NumberFormatException e) {
//use this if we're unable to collect the number of runs as a string.
setShowBarWithError(numberOfErrors + numberOfFailures > 0, numberOfRuns > 0, currentRun.getFinished());
}
fCounterPanel.setRunValue(numberOfRuns, totalNumberOfRuns);
fCounterPanel.setErrorValue(numberOfErrors);
fCounterPanel.setFailureValue(numberOfFailures);
String totalTime = currentRun.getTotalTime();
if (totalTime == null) {
Collection<PyUnitTestStarted> testsRunning = currentRun.getTestsRunning();
FastStringBuffer bufStatus = new FastStringBuffer("Current: ", 200);
FastStringBuffer bufTooltip = new FastStringBuffer("Current: ", 200);
int i = 0;
for (PyUnitTestStarted pyUnitTestStarted : testsRunning) {
if (i > 0) {
bufTooltip.append('\n');
}
bufTooltip.append(pyUnitTestStarted.test);
bufTooltip.append(" ");
bufTooltip.append('(');
bufTooltip.append(pyUnitTestStarted.location);
bufTooltip.append(')');
if (i > 0) {
bufStatus.append(", ");
}
bufStatus.append(pyUnitTestStarted.test);
i++;
}
this.fStatus.setText(bufStatus.toString());
this.fStatus.setToolTipText(bufTooltip.toString());
} else {
this.fStatus.setText(totalTime);
this.fStatus.setToolTipText(totalTime);
}
} else {
this.fStatus.setText("");
this.fStatus.setToolTipText("");
fCounterPanel.setRunValue(0, "0");
fCounterPanel.setErrorValue(0);
fCounterPanel.setFailureValue(0);
setShowBarWithError(false, false, false);
}
}
/**
* Helper method to reset the bar to a state knowing only about if we have errors, runs and whether it's finished.
*
* Only really used if we have no errors or if we don't know how to collect the current number of test runs.
*/
private void setShowBarWithError(boolean hasError, boolean hasRuns, boolean finished) {
fProgressBar.reset(hasError, false, hasRuns ? 1 : 0, finished ? 1 : 2);
}
/**
* Selection listener added to the tree so that the text output is updated when the selection changes.
*/
private final class SelectResultSelectionListener extends SelectionAdapter {
@Override
public void widgetSelected(SelectionEvent e) {
if (e.item != null) {
PyUnitTestResult result = (PyUnitTestResult) e.item.getData(PY_UNIT_TEST_RESULT);
onSelectResult(result);
}
}
}
/**
* Should only be used in the onSelectResult.
*/
private final FastStringBuffer tempOnSelectResult = new FastStringBuffer();
private final String ERRORS_HEADER = "============================= ERRORS =============================\n";
private final String CAPTURED_OUTPUT_HEADER = "======================== CAPTURED OUTPUT =========================\n";
/**
* Called when a test is selected in the tree (shows its results in the text output text component).
* Makes the line tracker aware of the changes so that links are properly created.
*/
/*default*/void onSelectResult(PyUnitTestResult result) {
tempOnSelectResult.clear();
boolean addedErrors = false;
if (result != null) {
if (result.errorContents != null && result.errorContents.length() > 0) {
addedErrors = true;
tempOnSelectResult.append(ERRORS_HEADER);
tempOnSelectResult.append(result.errorContents);
}
if (result.capturedOutput != null && result.capturedOutput.length() > 0) {
if (tempOnSelectResult.length() > 0) {
tempOnSelectResult.append("\n");
}
tempOnSelectResult.append(CAPTURED_OUTPUT_HEADER);
tempOnSelectResult.append(result.capturedOutput);
}
}
String string = tempOnSelectResult.toString();
testOutputText.setFont(JFaceResources.getFont(IDebugUIConstants.PREF_CONSOLE_FONT));
testOutputText.setText(string);
testOutputText.setStyleRange(new StyleRange());
if (addedErrors) {
StyleRange range = new StyleRange();
//Set the proper color if it's available.
TextAttribute errorTextAttribute = ColorManager.getDefault().getConsoleErrorTextAttribute();
if (errorTextAttribute != null) {
range.foreground = errorTextAttribute.getForeground();
}
range.start = ERRORS_HEADER.length();
range.length = result.errorContents.length();
testOutputText.setStyleRange(range);
}
lineTracker.splitInLinesAndAppendToLineTracker(string);
}
/**
* Activates the link that was clicked (if the given style range actually has a link).
*/
private static final class ActivateLinkmouseListener extends MouseAdapter {
@Override
public void mouseUp(MouseEvent e) {
Widget w = e.widget;
if (w instanceof StyledText) {
StyledText styledText = (StyledText) w;
int offset = styledText.getCaretOffset();
if (offset >= 0 && offset < styledText.getCharCount()) {
StyleRange styleRangeAtOffset = styledText.getStyleRangeAtOffset(offset);
if (styleRangeAtOffset instanceof StyleRangeWithCustomData) {
StyleRangeWithCustomData styleRangeWithCustomData = (StyleRangeWithCustomData) styleRangeAtOffset;
Object l = styleRangeWithCustomData.customData;
if (l instanceof IHyperlink) {
((IHyperlink) l).linkActivated();
}
}
}
}
}
}
/**
* Makes the test double clicked in the tree active in the editor.
*/
private final class DoubleClickTreeItemMouseListener extends MouseAdapter {
@Override
public void mouseDoubleClick(MouseEvent e) {
if (e.widget == tree) {
onTriggerGoToTest();
}
}
}
/**
* Makes the test with the enter pressed in the tree active in the editor.
*/
private final class EnterProssedTreeItemKeyListener extends KeyAdapter {
@Override
public void keyReleased(KeyEvent e) {
if (e.widget == tree && (e.keyCode == SWT.LF || e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR)) {
onTriggerGoToTest();
}
}
}
/**
* Makes the test currently selected in the tree the active test in the editor.
*/
public void onTriggerGoToTest() {
TreeItem[] selection = tree.getSelection();
if (selection.length >= 1) {
PyUnitTestResult result = (PyUnitTestResult) selection[0].getData(PY_UNIT_TEST_RESULT);
result.open();
}
}
/**
* Relaunches the currently selected tests.
*/
public void relaunchSelectedTests(String mode) {
TreeItem[] selection = tree.getSelection();
List<PyUnitTestResult> resultsToRelaunch = new ArrayList<PyUnitTestResult>();
PyUnitTestRun testRun = null;
for (TreeItem item : selection) {
PyUnitTestResult result = (PyUnitTestResult) item.getData(PY_UNIT_TEST_RESULT);
if (testRun == null) {
testRun = result.getTestRun();
} else {
if (result.getTestRun() != testRun) {
continue; //all must be part of the same test run -- this shouldn't really happen.
}
}
resultsToRelaunch.add(result);
}
if (resultsToRelaunch.size() > 0 && testRun != null) {
testRun.relaunch(resultsToRelaunch, mode);
}
}
/**
* @return the current test run.
*/
public PyUnitTestRun getCurrentTestRun() {
return this.currentRun;
}
/**
* Sets the current run (updates the UI)
*
* Note that it can be called to update the current test run when changing whether only errors should be
* shown or not (so, we don't check if it's the current or not, just go on and update all).
*/
public void setCurrentRun(PyUnitTestRun testRun) {
this.currentRun = testRun;
PyUnitViewTestsHolder.setCurrentTest(testRun);
tree.setRedraw(false);
try {
tree.removeAll();
testOutputText.setText(""); //Clear initial results (the first added will be selected)
if (testRun != null) {
List<PyUnitTestResult> sharedResultsList = testRun.getSharedResultsList();
for (PyUnitTestResult result : sharedResultsList) {
notifyTest(result, false);
}
}
updateCountersAndBar();
} finally {
tree.setRedraw(true);
}
}
/**
* @return returns a copy with the test runs available.
*/
public List<PyUnitTestRun> getAllTestRuns() {
synchronized (PyUnitViewTestsHolder.lockServerListeners) {
ArrayList<PyUnitTestRun> ret = new ArrayList<PyUnitTestRun>();
for (PyUnitViewServerListener listener : PyUnitViewTestsHolder.serverListeners) {
ret.add(listener.getTestRun());
}
return ret;
}
}
/**
* Sets whether only errors should be shown.
*/
public void setShowOnlyErrors(boolean b) {
this.showOnlyErrors = b;
PydevDebugPlugin.getDefault().getPreferenceStore().setValue(PYUNIT_VIEW_SHOW_ONLY_ERRORS, b);
this.setCurrentRun(currentRun); //update all!
}
/**
* Removes all the terminated test runs.
*/
public void clearAllTerminated() {
synchronized (PyUnitViewTestsHolder.lockServerListeners) {
boolean removedCurrent = false;
for (Iterator<PyUnitViewServerListener> it = PyUnitViewTestsHolder.serverListeners.iterator(); it
.hasNext();) {
PyUnitTestRun next = it.next().getTestRun();
if (next.getFinished()) {
if (next == this.currentRun) {
removedCurrent = true;
}
it.remove();
}
}
if (removedCurrent) {
if (PyUnitViewTestsHolder.serverListeners.size() > 0) {
this.setCurrentRun(PyUnitViewTestsHolder.serverListeners.getFirst().getTestRun());
} else {
this.setCurrentRun(null);
}
}
}
}
/**
* Sets the text component to be used (in tests we want to set it externally)
*/
/*default*/void setTextComponent(StyledText text) {
this.testOutputText = text;
onControlCreated.call(text);
text.addMouseListener(this.activateLinkmouseListener);
}
@Override
public ICallbackWithListeners getOnControlCreated() {
return onControlCreated;
}
@Override
public ICallbackWithListeners getOnControlDisposed() {
return onControlDisposed;
}
public void exportCurrentToClipboard() {
PyUnitTestRun curr = this.currentRun;
if (curr == null) {
return;
}
String str = curr.toXML();
if (str.length() > 0) {
new ClipboardHandler().putIntoClipboard(DND.CLIPBOARD, Display.getCurrent(), str);
}
}
public void restoreFromClipboard() {
try {
String clipboardContents = ClipboardHandler.getClipboardContents();
PyUnitTestRun testRunRestored = PyUnitTestRun.fromXML(clipboardContents);
DummyPyUnitServer pyUnitServer = new DummyPyUnitServer(testRunRestored.getPyUnitLaunch());
final PyUnitViewServerListener serverListener = new PyUnitViewServerListener(pyUnitServer, testRunRestored);
PyUnitViewTestsHolder.addServerListener(serverListener);
this.setCurrentRun(testRunRestored);
} catch (Exception e) {
Log.log(e);
Status status = PydevPlugin.makeStatus(IStatus.ERROR, e.getMessage(), e);
ErrorDialog.openError(EditorUtils.getShell(), "Error restoring tests from clipboard",
"Error restoring tests from clipboard", status);
}
}
}