diff --git a/lucene/src/test-framework/org/apache/lucene/util/LuceneTestCase.java b/lucene/src/test-framework/org/apache/lucene/util/LuceneTestCase.java index 1dea2be..4aebfc5 100644 --- a/lucene/src/test-framework/org/apache/lucene/util/LuceneTestCase.java +++ b/lucene/src/test-framework/org/apache/lucene/util/LuceneTestCase.java @@ -551,6 +551,16 @@ public abstract class LuceneTestCase extends Assert { protected static boolean testsFailed; /* true if any tests failed */ + /** + * A custom rule to timeout the test after a given period of time. + */ + @Rule + public final transient TimeoutRule timeout = new TimeoutRule() { + protected String formatObjectDetails(Object target) { + return reportAdditionalFailureInfo(); + } + }; + // This is how we get control when errors occur. // Think of this as start/end/success/failed // events. @@ -1322,10 +1332,12 @@ public abstract class LuceneTestCase extends Assert { } // We get here from InterceptTestCaseEvents on the 'failed' event.... - public void reportAdditionalFailureInfo() { - System.err.println("NOTE: reproduce with: ant test -Dtestcase=" + getClass().getSimpleName() - + " -Dtestmethod=" + getName() + " -Dtests.seed=" + new ThreeLongs(staticSeed, seed, LuceneTestCaseRunner.runnerSeed) - + reproduceWithExtraParams()); + public String reportAdditionalFailureInfo() { + String info = "NOTE: reproduce with: ant test -Dtestcase=" + getClass().getSimpleName() + + " -Dtestmethod=" + getName() + " -Dtests.seed=" + new ThreeLongs(staticSeed, seed, LuceneTestCaseRunner.runnerSeed) + + reproduceWithExtraParams(); + System.err.println(info); + return info; } // extra params that were overridden needed to reproduce the command diff --git a/lucene/src/test-framework/org/apache/lucene/util/Timeout.java b/lucene/src/test-framework/org/apache/lucene/util/Timeout.java new file mode 100644 index 0000000..b625c2b --- /dev/null +++ b/lucene/src/test-framework/org/apache/lucene/util/Timeout.java @@ -0,0 +1,20 @@ +package org.apache.lucene.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Changes the default timeout of a given method if {@link TimeoutRule} is used. + * The test case must use {@link TimeoutRule} for this to work. + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Timeout { + /** + * Specify an approximate timeout in milliseconds (exact timeout may vary + * depending on the JVM). + */ + int millis() default TimeoutRule.TIMEOUT_DEFAULT; +} diff --git a/lucene/src/test-framework/org/apache/lucene/util/TimeoutRule.java b/lucene/src/test-framework/org/apache/lucene/util/TimeoutRule.java new file mode 100644 index 0000000..d593703 --- /dev/null +++ b/lucene/src/test-framework/org/apache/lucene/util/TimeoutRule.java @@ -0,0 +1,90 @@ +package org.apache.lucene.util; + +import org.junit.rules.MethodRule; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.Statement; + +/** + * A custom timeout rule for {@link LuceneTestCase} methods. + */ +class TimeoutRule implements MethodRule { + /** Default timeout. */ + private static final int MINUTE = 1000 * 60; + public static final int TIMEOUT_DEFAULT = 60 * MINUTE; + + /** + * Default timeout for all test class methods, unless overriden with + * {@link Timeout} annotation. + */ + private int defaultMillis; + + public TimeoutRule() { + this(TIMEOUT_DEFAULT); + } + + public TimeoutRule(int defaultTimeoutMillis) { + if (defaultTimeoutMillis <= 0) throw new IllegalArgumentException( + "Timeout must be >= 0: " + defaultTimeoutMillis); + + this.defaultMillis = defaultTimeoutMillis; + } + + @Override + public Statement apply(final Statement base, final FrameworkMethod method, + final Object target) { + return new Statement() { + volatile Throwable nested; + + @SuppressWarnings("deprecation") + public void evaluate() throws Throwable { + /* + * Run the test case in a spawned thread (the cost shouldn't matter much). + */ + final int timeout = getExactTimeout(method, target); + final Thread t = new Thread() { + public void run() { + try { + base.evaluate(); + } catch (Throwable e) { + nested = e; + } + } + }; + + // we don't prevent the jvm from quitting, even if we try to clean up. + t.setDaemon(true); + t.start(); + t.join(timeout); + + if (t.isAlive()) { + // Still alive after timeout? Collect info, kill the thread and fail. + final String objectDetails = formatObjectDetails(target); + final String message = "Timeout of " + timeout + + " milliseconds exceeded. Object details:\n" + objectDetails; + nested = new AssertionError(message); + nested.setStackTrace(t.getStackTrace()); + + // Try to interrupt or kill t, whatever. + t.stop(); + throw nested; + } + + if (nested != null) throw nested; + } + }; + } + + /** Provide more explicit details about the target object if possible. */ + protected String formatObjectDetails(Object target) { + return target.toString(); + } + + /** */ + private int getExactTimeout(FrameworkMethod method, Object target) { + Timeout t = method.getMethod().getAnnotation(Timeout.class); + int timeout = (t != null ? t.millis() : defaultMillis); + if (timeout <= 0) throw new IllegalArgumentException( + "Timeout must be >= 0: " + timeout); + return timeout; + } +} diff --git a/lucene/src/test/org/apache/lucene/util/TestTimeouts.java b/lucene/src/test/org/apache/lucene/util/TestTimeouts.java new file mode 100644 index 0000000..c86bd11 --- /dev/null +++ b/lucene/src/test/org/apache/lucene/util/TestTimeouts.java @@ -0,0 +1,57 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.lucene.util; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.JUnitCore; +import org.junit.runner.Result; + +public class TestTimeouts { + + public static class Nested extends LuceneTestCase { + @Timeout(millis = 500) + public void testJunit3SpinLoop() { + testJunit4SpinLoop(); + } + + @Timeout(millis = 500) + public void testJunit3ThreadSleep() throws Exception { + testJunit4ThreadSleep(); + } + + @Test + @Timeout(millis = 500) + public void testJunit4SpinLoop() { + while (true) + /* count electric sheep */; + } + + @Test + @Timeout(millis = 500) + public void testJunit4ThreadSleep() throws Exception { + Thread.sleep(5000); + } + } + + @Test + public void testTimeouts() { + Result result = JUnitCore.runClasses(Nested.class); + Assert.assertEquals(4, result.getFailures().size()); + } +}