/* * Created on 31.03.2005 * by Richard Birenheide (D035816) * * Copyright SAP AG 2005 */ package junit.extensions; import junit.framework.AssertionFailedError; import junit.framework.Test; import junit.framework.TestResult; import junit.framework.TestSuite; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Shell; import com.windowtester.runtime.swt.internal.operation.SWTPushEventOperation; /** * A test suite for running PDE tests in a separate thread. * <p/> * Forks the entire test run to run in a new thread.<br> * Intended for usage with Eclipse jUnit run/debug configurations. These * run normally in the UI thread which blocks on showing dialogs or popup menues. * Eclipse can recognize classes having a method <code>public static Test suite()</code> * Within that one can code:<br/><code><pre> * public static Test suite() { * ActivePDETestSuite suite = new ActivePDETestSuite("name"); * //Testclasses derived from org.junit.TestCase * suite.addTestSuite(FirstTestClass.class); * suite.addTestSuite(SecondTestClass.class); * . * . * . * return suite; * } * </pre> * </code> * @author Richard Birenheide (D035816) */ public class ActivePDETestSuite extends TestSuite { 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; /** * The current implementation uses ESC key strokes to close eventually left open * stuff when a tests fails. Tom make this customizable, the number of strokes * in each tear down is stored in this variable. The standard is 5. If this * is too few strokes (more windows could be open) or too much (not necessary * under any circumstances and causing too much overhead in long running tests) * the value can be set accordingly. This can be done in {@link #suiteSetUp()}. */ protected int escapeStrokes = 5; /** * Default constructor. * <p/> * The name associated with this class is given the Class name. */ public ActivePDETestSuite() { super(ActivePDETestSuite.class.getName()); } /** * Constructs with a test class. * <p/> * The name associated with this class is given the Class name. * @param theClass a test class. */ public ActivePDETestSuite(Class theClass) { super(theClass, ActivePDETestSuite.class.getName()); } /** * Constructs with a name containing no test. * <p/> * @param name the name. This name will be given to the separate thread running. */ public ActivePDETestSuite(String name) { super (name); } /** * 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 ActivePDETestSuite(Class theClass, String name) { super(theClass, name); } public void run(final TestResult result) { this.preForkSetUp(); 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(); Thread t = new Thread(this.getName()) { public void run() { try { ActivePDETestSuite.this.suiteSetUp(); ActivePDETestSuite.super.run(result); ActivePDETestSuite.this.suiteTearDown(); } finally { ActivePDETestSuite.this.testsFinished = true; display.wake(); } } }; t.setDaemon(true); t.start(); waitUntilFinished(); } public void runTest(final Test test, final TestResult result) { try { // inlined due to limitation in VA/Java //ActiveTestSuite.super.runTest(test, result); test.run(result); } finally { ActivePDETestSuite.this.runFinished(); } } private void waitUntilFinished() { int ctr = 0; while (!this.testsFinished) { try { if(!display.readAndDispatch()) display.sleep(); } catch (SWTException ex) { //Do nothing: rethrowing errors blocks the display thread. //One must rely on the fact of proper error handling of //display thread users. } if(ctr++%100000==0) System.err.print("."); } } /** * Closes all shells when finished. */ private void runFinished() { Runnable closeShells = new Runnable() { public void run() { if (!rootShell.isDisposed()) { //Close all blocking dialogs. Necessary, otherwise the junit //thread does not proceed. Shell[] shells = rootShell.getShells(); ActivePDETestSuite.closeShells(shells); } //TODO close all open stuff eg. menues etc. //Introduced as work around until a way is found to close open //menu windows. //Robot robot = new Robot(); for (int i = 0; i < escapeStrokes; i++) { keyClick(SWT.ESC); } } }; display.syncExec(closeShells); } /** * 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 forking into a separate thread. * <p/> * This method is useful for tests which initially do not run in * the display thread. It is expected that in that case the calling * thread is made to the display thread by calling {@link Display#getDefault()} * or makes the current thread to the display thread by any other means.<br/> * The default implementation does nothing. */ protected void preForkSetUp() {} /** * Runs a set up prior to executing the entire tests within this suite. * <p/> * The method will run in the separate thread.<br/> * The default implementation does nothing. */ protected void suiteSetUp() {} /** * Runs a tear down after executing the entire tests within this suite. * <p/> * The method will run in the separate thread.<br/> * The default implementation does nothing. */ protected void suiteTearDown() {} /** * 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 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(Display display, final Runnable runnable) { try { display.syncExec(runnable); } catch (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 runnable the Runnable to execute. */ public static void asyncExec(Display display, Runnable runnable) { try { display.asyncExec(runnable); } catch (SWTException swtEx) { if (swtEx.throwable instanceof AssertionFailedError) { throw (AssertionFailedError) swtEx.throwable; } else { throw swtEx; } } } //////////////////////////////////////////////////////////////////////////// // // Primitive event posting actions // //////////////////////////////////////////////////////////////////////////// /** * Dispatch a keyClick(keyUp..keyDown) event. * @param keyCode - the key to click. */ public void keyClick(int keyCode) { keyDown(keyCode); keyUp(keyCode); } /** * Dispatch a keyClick(keyUp..keyDown) event. * @param keyCode - the key to click. */ public void keyClick(char keyCode) { boolean shift = needsShift(keyCode); if (shift) keyDown(SWT.SHIFT); keyDown(keyCode); keyUp(keyCode); if (shift) keyUp(SWT.SHIFT); } /** * Determine if this key requires a shift to dispatch the keyStroke. * @param keyCode - the key in question * @return true if a shift event is required. */ boolean needsShift(char keyCode) { if (keyCode >= 62 && keyCode <=90) return true; if (keyCode >= 123 && keyCode <=126) return true; if (keyCode >= 33 && keyCode <=43 && keyCode != 39) return true; if (keyCode >= 94 && keyCode <=95) return true; if (keyCode == 58 || keyCode == 60 || keyCode == 62) return true; return false; } /** * Dispatch a keyUp event. * @param keyCode - the key to release. */ public void keyUp(final int keyCode) { //trace("post key up " + keyCode); Event event = new Event(); event.type = SWT.KeyUp; event.keyCode = keyCode; new SWTPushEventOperation(event).execute(); } /** * Dispatch a keyDown event. * @param keyCode - the key to press. */ public void keyDown(final int keyCode) { //trace("post key down " + keyCode); Event event = new Event(); event.type = SWT.KeyDown; event.keyCode = keyCode; new SWTPushEventOperation(event).execute(); } /** * Dispatch a keyUp event. * @param keyCode - the key to release. */ public void keyUp(final char keyCode) { //trace("post key up " + keyCode); Event event = new Event(); event.type = SWT.KeyUp; event.character = keyCode; new SWTPushEventOperation(event).execute(); } /** * Dispatch a keyDown event. * @param keyCode - the key to press. */ public void keyDown(final char keyCode) { //trace("post key down " + keyCode); Event event = new Event(); event.type = SWT.KeyDown; event.character = keyCode; new SWTPushEventOperation(event).execute(); } }