package com.rcpcompany.test.utils.ui.rules; import static org.junit.Assert.assertNotNull; import java.util.concurrent.TimeUnit; import org.eclipse.swt.widgets.Display; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; /** * The SWTTimeout Rule applies the same timeout to all test methods in a class in exactly the same way as * {@link org.junit.rules.Timeout Timeout}. * <p> * The difference is that this rule works with SWT by evaluating the original statement in the original {@link Thread}, * whereas {@link org.junit.rules.Timeout Timeout} creates a special {@link Thread} for this. This version requires * {@link Display#getDefault()} returns a display. * <p> * This rule works by * <ul> * <li>using {@link Display#timerExec(int, Runnable)} to interrupt the UI queue</li> * <li>creating a special {@link Thread} to interrupt wait and IO operations</li> * </ul> * This rule can hang in the same cases as {@link org.junit.rules.Timeout} - if tested {@link Statement} involves an * endless loop or a similar operation that never terminates. */ public class SWTTimeout implements TestRule { /** * The timeout in milliseconds. */ protected final int myTimeout; /** * @param millis * the millisecond timeout */ public SWTTimeout(int millis) { myTimeout = millis; } /** * @param timeout * the timeout * @param unit * the unit */ public SWTTimeout(int timeout, TimeUnit unit) { this((int) unit.toMillis(timeout)); } @Override public Statement apply(Statement base, Description description) { return new SWTTimeoutStatement(base, description); } private class SWTTimeoutStatement extends Statement { private final Statement myOriginalStatement; private final Description myDescription; protected Display myDisplay = null; protected volatile boolean inEval = false; public SWTTimeoutStatement(Statement originalStatement, Description description) { myOriginalStatement = originalStatement; myDescription = description; } @Override public void evaluate() throws Throwable { myDisplay = Display.getDefault(); assertNotNull("No Display found. Make sure you run this test in RAP or SWT", myDisplay); myDisplay.timerExec(myTimeout, new Runnable() { @Override public void run() { if (inEval) throw new TimeoutException(); } }); final Thread timeoutThread = new Thread() { @Override public void run() { try { sleep(myTimeout); } catch (final InterruptedException ex) { /* * The timeout thread is interrupted when the original statement has been evaluated */ return; } final Thread displayThread = myDisplay.getThread(); if (!inEval || displayThread == null || !displayThread.isAlive()) return; displayThread.interrupt(); }; }; timeoutThread.start(); try { try { inEval = true; myOriginalStatement.evaluate(); } finally { inEval = false; timeoutThread.interrupt(); timeoutThread.join(); } } catch (final TimeoutException ex) { throwTimeoutException(ex, "ui event queue"); } catch (final InterruptedException ex) { throwTimeoutException(ex, "thread interrupted"); } } /** * Throws a new exception based on the specified {@link Throwable}. * <p> * Copies over the stack trace. * * @param ex * the original exception * @param reason * the reason for the timeout * @throws Exception * the new exception */ private void throwTimeoutException(final Throwable ex, String reason) throws Exception { final Exception exception = new Exception(String.format("test timed out after %d milliseconds (%s)", myTimeout, reason)); exception.setStackTrace(ex.getStackTrace()); throw exception; } /** * Local exception class used to signal timeout in the UI thread. */ @SuppressWarnings("serial") protected class TimeoutException extends RuntimeException { } } }