JUnit infrastructure now properly handles exceptions thrown from event handlers
that are triggered synchronously from within test methods (e.g. by calling
Button.click()).
Issue: 3258
Patch by: jgw
Review by: scottb
git-svn-id: https://google-web-toolkit.googlecode.com/svn/releases/1.6@4496 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/GWTTestCase.java b/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/GWTTestCase.java
index 9b592c8..7f166f0 100644
--- a/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/GWTTestCase.java
+++ b/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/GWTTestCase.java
@@ -62,6 +62,37 @@
}
/**
+ * UncaughtExceptionHandler used to catch exceptions thrown out of Javascript
+ * event handlers.
+ */
+ private final class TestCaseUncaughtExceptionHandler implements
+ UncaughtExceptionHandler {
+
+ // Holds the first exception that's throws "synchronously", meaning "before
+ // the test method returns".
+ private Throwable synchronousException = null;
+
+ /**
+ * An uncaught exception escaped to the browser; what we should do depends
+ * on what state we're in.
+ */
+ public void onUncaughtException(Throwable ex) {
+ if (mainTestHasRun && timer != null) {
+ // Asynchronous mode; uncaught exceptions cause an immediate failure.
+ assert (!testIsFinished);
+ reportResultsAndRunNextMethod(ex);
+ } else {
+ // Synchronous mode: hang on to it for after the test method returns.
+ // We can't call reportResultsAndRunNextMethod() yet, as it will cause
+ // a race condition that often causes the same test to be run again.
+ if (synchronousException == null) {
+ synchronousException = ex;
+ }
+ }
+ }
+ };
+
+ /**
* The collected checkpoint messages.
*/
private List<String> checkPoints;
@@ -86,21 +117,11 @@
*/
private KillTimer timer;
- private final UncaughtExceptionHandler uncaughtHandler = new UncaughtExceptionHandler() {
- /**
- * An uncaught exception escaped to the browser; what we should do depends
- * on what state we're in.
- */
- public void onUncaughtException(Throwable ex) {
- if (mainTestHasRun && timer != null) {
- // Asynchronous mode; uncaught exceptions cause an immediate failure.
- assert (!testIsFinished);
- reportResultsAndRunNextMethod(ex);
- } else {
- // just ignore it
- }
- }
- };
+ /**
+ * The UncaughtExceptionHandler that will be used to catch exceptions thrown
+ * from event handlers. We will create a new one for each test method.
+ */
+ private TestCaseUncaughtExceptionHandler uncaughtHandler;
// CHECKSTYLE_OFF
/**
@@ -111,7 +132,7 @@
if (shouldCatchExceptions()) {
// Make sure no exceptions escape
- GWT.setUncaughtExceptionHandler(uncaughtHandler);
+ GWT.setUncaughtExceptionHandler(uncaughtHandler = new TestCaseUncaughtExceptionHandler());
try {
runBare();
} catch (Throwable e) {
@@ -127,6 +148,13 @@
// timer != null we are in true asynchronous mode.
mainTestHasRun = true;
+ // See if any synchronous exceptions got picked up by the UncaughtExceptionHandler.
+ if ((uncaughtHandler != null) && (uncaughtHandler.synchronousException != null)) {
+ // If an exception was caught in an event handler, it must have happened
+ // before the exception was thrown from the test method.
+ caught = uncaughtHandler.synchronousException;
+ }
+
if (caught != null) {
// Test failed; finish test no matter what state we're in.
reportResultsAndRunNextMethod(caught);
@@ -262,6 +290,10 @@
// ignore any exceptions thrown from tearDown
}
+ // Remove the UncaughtExceptionHandler we may have installed in __doRunTest.
+ GWT.setUncaughtExceptionHandler(null);
+ uncaughtHandler = null;
+
JUnitResult myResult = __getOrCreateTestResult();
if (ex != null) {
ExceptionWrapper ew = new ExceptionWrapper(ex);
diff --git a/user/test/com/google/gwt/junit/client/TestManualAsync.java b/user/test/com/google/gwt/junit/client/TestManualAsync.java
index f4d62c5..4e9ae19 100644
--- a/user/test/com/google/gwt/junit/client/TestManualAsync.java
+++ b/user/test/com/google/gwt/junit/client/TestManualAsync.java
@@ -18,6 +18,10 @@
import static com.google.gwt.junit.client.GWTTestCaseTest.SetUpTearDownState.IS_SETUP;
import static com.google.gwt.junit.client.GWTTestCaseTest.SetUpTearDownState.IS_TORNDOWN;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.EventListener;
import com.google.gwt.user.client.Timer;
/**
@@ -27,6 +31,91 @@
*/
public class TestManualAsync extends GWTTestCaseTest {
+ // The following tests (all prefixed with test_) are intended to test the
+ // interaction of synchronous failures (within event handlers) with various
+ // other types of failures and successes. All of them are expected to fail
+ // with the message "Expected failure".
+ //
+ // Nomenclature for these tests:
+ // DTF => delayTestFinish()
+ // SF => synchronous failure (from event handler)
+ // FT => finishTest()
+ // F => fail()
+ // R => return;
+
+ public void test_dtf_sf() {
+ System.out.println("test_dtf_sf");
+ delayTestFinish();
+ synchronousFailure("test_dtf_sf");
+ }
+
+ public void test_dtf_sf_f() {
+ System.out.println("test_dtf_sf_f");
+ delayTestFinish();
+ synchronousFailure("test_dtf_sf_f");
+ failNow("test_dtf_sf_f");
+ }
+
+ public void test_dtf_sf_ft() {
+ System.out.println("test_dtf_sf_ft");
+ delayTestFinish();
+ synchronousFailure("test_dtf_sf_ft");
+ finishTest();
+ }
+
+ public void test_dtf_sf_r_f() {
+ System.out.println("test_dtf_sf_r_f");
+ delayTestFinish();
+ synchronousFailure("test_dtf_sf_r_f");
+ failLater("test_dtf_sf_r_f");
+ }
+
+ public void test_dtf_sf_r_ft() {
+ System.out.println("test_dtf_sf_r_ft");
+ delayTestFinish();
+ synchronousFailure("test_dtf_sf_r_ft");
+ finishTestLater("test_dtf_sf_r_ft");
+ }
+
+ public void test_sf() {
+ System.out.println("test_sf");
+ synchronousFailure("test_sf");
+ }
+
+ public void test_sf_dtf_f() {
+ System.out.println("test_sf_dtf_f");
+ synchronousFailure("test_sf_dtf_f");
+ delayTestFinish();
+ failNow("test_sf_dtf_f");
+ }
+
+ public void test_sf_dtf_ft() {
+ System.out.println("test_sf_dtf_ft");
+ synchronousFailure("test_sf_dtf_ft");
+ delayTestFinish();
+ finishTest();
+ }
+
+ public void test_sf_dtf_r_f() {
+ System.out.println("test_sf_dtf_r_f()");
+ synchronousFailure("test_sf_dtf_r_f");
+ delayTestFinish();
+ failLater("test_sf_dtf_r_f");
+ }
+
+ public void test_sf_dtf_r_ft() {
+ System.out.println("test_sf_dtf_r_ft()");
+ synchronousFailure("test_sf_dtf_r_ft");
+ delayTestFinish(5 * 60 * 1000);
+ finishTestLater("test_sf_dtf_r_ft");
+ }
+
+ public void test_sf_f() {
+ System.out.println("test_sf_f()");
+ synchronousFailure("test_sf_f");
+ failNow("test_sf_f");
+ }
+
/**
* Fails normally.
*/
@@ -55,7 +144,7 @@
* Async fails.
*/
public void testFailAsync() {
- delayTestFinish(200);
+ delayTestFinish(1000);
new Timer() {
public void run() {
fail("Expected failure");
@@ -64,6 +153,41 @@
}
/**
+ * Tests the case where a JUnit exception is thrown from an event handler, but
+ * after this test method has completed successfully.
+ *
+ * This test should *not* fail, but the next one should.
+ */
+ public void testLateFailPart1() {
+ // Leave the test in synchronous mode, but crank up a timer to fail in 2.5s.
+ new Timer() {
+ @Override
+ public void run() {
+ // This fail should be called during the next test.
+ fail();
+ }
+ }.schedule(2500);
+
+ // We don't actually assert anything here. This test exists solely to make
+ // the next one fail.
+ }
+
+ /**
+ * Second half of the previous test.
+ */
+ public void testLateFailPart2() {
+ // Go into async mode from 5s, finishing in 4. The timer from the previous
+ // test will go off and call fail() before finishTest() is called.
+ delayTestFinish(5000);
+ new Timer() {
+ @Override
+ public void run() {
+ finishTest();
+ }
+ }.schedule(4000);
+ }
+
+ /**
* Completes normally.
*/
public void testNormal() {
@@ -182,4 +306,55 @@
}.schedule(200);
}
+ // Call delayTestFinish() with enough time so that failLater() will
+ // definitely fail.
+ private void delayTestFinish() {
+ delayTestFinish(2500);
+ }
+
+ // Fail asynchronously after a small amount of time.
+ private void failLater(final String failMsg) {
+ new Timer() {
+ @Override
+ public void run() {
+ System.out.println("failLater(): " + failMsg);
+ failNow(failMsg);
+ }
+ }.schedule(100);
+ }
+
+ // Fail synchronously with an "expected failure" message.
+ private void failNow(String failMsg) {
+ fail("Expected failure (" + failMsg + ")");
+ }
+
+ // Finish the test asynchronously after a small amount of time.
+ private void finishTestLater(final String debugMsg) {
+ System.out.println("finishTestLater(): " + debugMsg);
+ new Timer() {
+ @Override
+ public void run() {
+ System.out.println("finishTestLater() done: " + debugMsg);
+ finishTest();
+ }
+ }.schedule(1);
+ }
+
+ // Trigger a test failure synchronously, but from within an event handler.
+ // (The exception thrown from fail() will get caught by the GWT
+ // UncaughtExceptionHandler).
+ private void synchronousFailure(final String failMsg) {
+ DivElement elem = Document.get().createDivElement();
+ Document.get().getBody().appendChild(elem);
+ Event.sinkEvents(elem, Event.ONSCROLL);
+
+ EventListener listener = new EventListener() {
+ public void onBrowserEvent(Event event) {
+ System.out.println("failNow(): " + failMsg);
+ failNow(failMsg);
+ }
+ };
+ Event.setEventListener(elem, listener);
+ Event.triggerScrollEvent(elem);
+ }
}