blob: b0c109e514d80d5e8ba453add9fea39c32f09802 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010 SAP AG, Walldorf.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* SAP AG - initial API and implementation
*******************************************************************************/
package org.eclipse.platform.discovery.testutils.utils.abbot;
import java.awt.AWTException;
import java.awt.Robot;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestResult;
import junit.framework.TestSuite;
import org.eclipse.platform.discovery.testutils.internal.plugin.TestPlugin;
import org.eclipse.platform.discovery.testutils.utils.abbot.util.TimerUtils;
import org.eclipse.platform.discovery.testutils.utils.abbot.util.internal.UICleanupManager;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
/**
* Suite for UI tests in SWT.
* <p>
* Forks the test execution out of the UI thread in order to avoid that a test
* is blocked because of modal dialogs. The suite itself has to be started in
* the UI thread. Consequently one cannot nest <code>ActiveSWTTestSuite</code>s.
* </p>
* <p>
* By default remaining dialogs will be closed by hitting escape a number of
* times. You can change that behavior by calling
* <code>setCloseShells(true)</code> in {@link #setUpPDE()} or earlier. As a
* consequence all shells that were not present before the test run will be
* closed by calling {@link Shell#close()}. This may however cause problems in
* subsequent tests if a shell is meant to be reused (e.g. GEF holds a shell in
* a static member).
* </p>
*
* @author Richard Birenheide
*/
public class ActiveSWTTestSuite extends TestSuite {
private class RunTestSuiteTask extends TimerTask {
private final TestResult result;
private final ExecutorService service;
public RunTestSuiteTask(TestResult result, ExecutorService service) {
super();
this.result = result;
this.service = service;
}
@Override
public void run() {
this.result.addError(ActiveSWTTestSuite.this, new Throwable("Test execution terminated to avoid JDTD timeout"));
try {
ActiveSWTTestSuite.this.suiteTearDown();
} catch (AssertionFailedError th) {
this.result.addFailure(ActiveSWTTestSuite.this, th);
} catch (Exception ex) {
this.result.addError(ActiveSWTTestSuite.this, ex);
}
ActiveSWTTestSuite.this.testsFinished = true;
ActiveSWTTestSuite.this.display.wake();
service.shutdownNow();
//System.exit(0);
}
}
private final UICleanupManager cleanupManager = new UICleanupManager();
// private final static long JDTD_TIME =
// System.getProperty("testrun.activeTimeout")
// == null ? 100000 :
// Long.parseLong(System.getProperty("testrun.activeTimeout"));
private volatile boolean testsFinished = false;
/**
* The display associated with this run.
*/
protected Display display;
/**
* The shell being active when starting this run.
*/
protected Shell rootShell;
/**
* Hold the number of tests in a test case that are already executed
*/
protected int executedTestsCount;
private TimerUtils timeController = TimerUtils.getInstance();
// a little less, than 15 mins, defined by JDTD itself
private final static long JDTD_TIMEOUT = 14 * 60 * 1000;
public TimerUtils getTimeController() {
return timeController;
}
public void setTimeController(TimerUtils timeController) {
this.timeController = timeController;
}
/**
* Default constructor.
* <p/>
* The name associated with this class is given the Class name.
*/
public ActiveSWTTestSuite() {
super(ActiveSWTTestSuite.class.getName());
initJDTDExecutionTimeControlling();
}
/**
* Constructs with a test class.
* <p/>
* The name associated with this class is given the Class name.
*
* @param theClass
* a test class.
*/
public ActiveSWTTestSuite(final Class<? extends TestCase> theClass) {
super(theClass, ActiveSWTTestSuite.class.getName());
initJDTDExecutionTimeControlling();
}
/**
* Constructs with a name containing no test.
* <p/>
*
* @param name
* the name. This name will be given to the separate thread
* running.
*/
public ActiveSWTTestSuite(final String name) {
super(name);
initJDTDExecutionTimeControlling();
}
/**
* Constructs with a name and containing the test class given.
* <p/>
*
* @param theClass
* a test class.
* @param name
* the name. This name will be given to the separate thread
* running.
*/
public ActiveSWTTestSuite(final Class<? extends TestCase> theClass, final String name) {
super(theClass, name);
initJDTDExecutionTimeControlling();
}
@Override
public final void run(final TestResult result) {
this.display = Display.getCurrent();
if (this.display == null) {
throw new IllegalStateException(
"The TestSuite must be run from an SWT UI thread");
}
this.rootShell = display.getActiveShell();
this.cleanupManager.registerUIState();
final ExecutorService service = Executors.newSingleThreadScheduledExecutor();
// handle JDTD timeout: stop the test before JDTD timeout is thrown
Timer timeoutTimer = new Timer();
TimerTask task = new RunTestSuiteTask(result, service);
timeoutTimer.schedule(task, TimerUtils.getInstance().getRemainingTime());
service.execute(new TestSuiteRunnerTask(result));
waitUntilFinished();
}
private void initJDTDExecutionTimeControlling() {
executedTestsCount = 0;
if (this.timeController.getJDTD_TIME() == 0) {
String jdtdProperty = System.getProperty("testrun.activeTimeout");
this.timeController
.setJDTD_TIME(jdtdProperty == null ? JDTD_TIMEOUT : Long
.parseLong(jdtdProperty) * 1000);
}
System.out.println("******** Expected end of test run: "
+ this.timeController.formatNowTime(this.timeController
.getJDTD_TIME()) + " ***********");
this.timeController.setSTART_TIME(System.currentTimeMillis());
}
private boolean shouldSkipNextTestExecution() {
long[] timeCheck = this.timeController.getRequiredTime(this
.countTestCases(), this.executedTestsCount);
return timeCheck[0] < timeCheck[1];
}
@Override
public final void runTest(final Test test, final TestResult result) {
try {
// inlined due to limitation in VA/Java
// ActiveSWTTestSuite.super.runTest(test, result);
test.run(result);
} finally {
ActiveSWTTestSuite.this.runFinished();
}
}
private void waitUntilFinished() {
while (!this.testsFinished) {
try {
if (!display.readAndDispatch()) {
display.sleep();
}
} catch (final SWTException ex) {
TestPlugin
.logError(
"A SWTException ocurred during waiting for the tests being finished in thread: "
+ Thread.currentThread().getName(), ex);
/*
* Do nothing: rethrowing errors leads to premature end of the
* WorkbenchTestable thread and the IDE subsequently not being
* shut down.
*/
} catch (final RuntimeException ex) {
TestPlugin
.logError(
"A RuntimeException ocurred during waiting for the tests being finished in thread: "
+ Thread.currentThread().getName(), ex);
}
}
}
/**
* Closes all shells when finished.
*/
private void runFinished() {
cleanupManager.cleanUp();
}
/**
* Retrieves the display associated with this test run.
* <p/>
*
* @return the display associated with this test run. Only valid after the
* test has been started.
*/
public Display getDisplay() {
return this.display;
}
/**
* Runs a set up prior to executing the entirety of the tests within this
* suite.
* <p>
* The method will run called in the non-UI thread. The default
* implementation does nothing.
* </p>
*
* @throws Exception
* convenience signature to ensure correct reporting on
* Exceptions in set up.
* {@link junit.framework.AssertionFailedError} is permissible
* as well and will be reported accordingly as failure.
*/
protected void suiteSetUp() throws Exception {
}
/**
* Runs a tear down after executing all tests within this suite.
* <p>
* The method will run called in the non-UI thread. The default
* implementation does nothing.
* </p>
*
* @throws Exception
* convenience signature to ensure correct reporting on
* Exceptions in tear down.
* {@link junit.framework.AssertionFailedError} is permissible
* as well and will be reported accordingly as failure.
*/
protected void suiteTearDown() throws Exception {
}
/**
* Closes all shells and child shells of the given array recursively.
* <p>
* This is called after each TestCase to guarantee that no (blocking)
* dialogs are still open. Does currently not work perfect and it is thus
* highly recommended that this is done properly in TestCase.tearDown().
*
* @param shells
* the shells to close.
*/
public static void closeShells(final Shell[] shells) {
for (int i = 0; i < shells.length; i++) {
if (!shells[i].isDisposed()) {
closeShells(shells[i].getShells());
}
if (!shells[i].isDisposed()) {
shells[i].close();
// shells[i].dispose();
}
}
}
/**
* Convenience method for {@link Display#syncExec(java.lang.Runnable)}
* catching {@link SWTException} and rethrowing {@link AssertionFailedError}
* if appropriate.
* <p>
* Should be used from TestCase.testXXX() methods when asserting within the
* SWT thread in order to guarantee that a test failure is displayed
* correctly.
*
* @param display
* the display to run the runnable in.
* @param runnable
* the Runnable to execute.
* @throws AssertionFailedError
* if an assertion failed in the display thread.
* @throws RuntimeException
* either a RuntimeException has been issued by the Runnable or
* the Runnable has thrown a Throwable not being a
* RuntimeException. In that case the RuntimeException carries
* the original Exception as cause.
*/
public static void syncExec(final Display display, final Runnable runnable) {
try {
display.syncExec(runnable);
} catch (final SWTException swtEx) {
if (swtEx.throwable instanceof AssertionFailedError) {
throw (AssertionFailedError) swtEx.throwable;
} else {
throw swtEx;
}
}
}
/**
* Convenience method for {@link Display#asyncExec(java.lang.Runnable)}
* catching {@link SWTException} and rethrowing {@link AssertionFailedError}
* if appropriate.
* <p>
* Should be used from TestCase.testXXX() methods when asserting within the
* SWT thread in order to guarantee that a test failure is displayed
* correctly.
* <p/>
* NOTE that exception handling with this method cannot be guaranteed to
* work since exceptions are thrown asynchronously. Currently I have no idea
* how to notify the caller of a test being failed. But generally I have no
* idea why one should like to run _tests_ asynchronously. Possibly one
* could introduce an ErrorListener here but I am not sure. Ideal would be
* to have knowledge about the actual {@link Test} and {@link TestResult}
* when this method is called. Then one could feed the result with
* {@link TestResult#addError(junit.framework.Test, java.lang.Throwable)} or
* {@link TestResult#addFailure(junit.framework.Test, junit.framework.AssertionFailedError)}
* . Actually I do not know how to get the correct Test. Unfortunately it is
* not the one issued by {@link #runTest(Test, TestResult)}.
*
* @param display
* the display to run the runnable in.
* @param runnable
* the Runnable to execute.
*/
public static void asyncExec(final Display display, final Runnable runnable) {
try {
display.asyncExec(runnable);
} catch (final SWTException swtEx) {
if (swtEx.throwable instanceof AssertionFailedError) {
throw (AssertionFailedError) swtEx.throwable;
} else {
throw swtEx;
}
}
}
/**
* Runs the test in the separate thread.
* <p/>
*
* @author Richard Birenheide
*/
private class TestSuiteRunnerTask implements Runnable {
private final TestResult result;
/**
* Constructs with name and the TestResult given.
* <p/>
*
* @param name
* the threads name.
* @param result
* the test result.
*/
private TestSuiteRunnerTask(final TestResult result) {
this.result = result;
// Prestart the AWT threads so that they will not be started in our
// thread group
try {
final Robot r = new Robot();
TestPlugin.logInfo("Robot created: " + r.toString());
} catch (final AWTException ex) {
TestPlugin.logError(ex);
}
}
public void run() {
// Safeguard the setting of the finish flag against errors.
// Otherwise the Test may block infinitely.
try {
ActiveSWTTestSuite.this.suiteSetUp();
if (!shouldSkipNextTestExecution()) {
ActiveSWTTestSuite.super.run(this.result);
} else {
throw new TimeoutExceededException(
"The remaining time is less than expected execution time");
}
} catch (final AssertionFailedError th) {
this.result.addFailure(ActiveSWTTestSuite.this, th);
} catch (final Exception ex) {
this.result.addError(ActiveSWTTestSuite.this, ex);
} finally {
try {
ActiveSWTTestSuite.this.suiteTearDown();
} catch (final Exception ex) {
this.result.addError(ActiveSWTTestSuite.this, ex);
} catch (final AssertionFailedError th) {
this.result.addFailure(ActiveSWTTestSuite.this, th);
}
ActiveSWTTestSuite.this.testsFinished = true;
ActiveSWTTestSuite.this.display.wake();
}
}
}
}