diff --git a/doc/source/development/testing.rst b/doc/source/development/testing.rst index 0f91629cf0..2ec1631e92 100644 --- a/doc/source/development/testing.rst +++ b/doc/source/development/testing.rst @@ -56,6 +56,11 @@ Long running tests Test that consume a significant amount of time during execution can be found in the ``test/long`` directory and executed as a regular JUnit test or standalone program. Except for the execution time, there’s nothing really special about them. However, ant will execute tests under ``test/long`` only when using the ``ant long-test`` target. +Flaky tests +----------- + +If a test failure is difficult to reproduce you can always use a shell loop, circle repeat strategy and similar solutions. At the JUnit level ``RepeatableRunner`` will let you run a JUnit class N times for convenience. On tests that are fast this is a much faster way to iterate than doing it at the shell level. Beware of tests that modify singleton state or similar as they won't work. + DTests ====== diff --git a/test/unit/org/apache/cassandra/RepeatableRunner.java b/test/unit/org/apache/cassandra/RepeatableRunner.java new file mode 100644 index 0000000000..75f3331fdf --- /dev/null +++ b/test/unit/org/apache/cassandra/RepeatableRunner.java @@ -0,0 +1,109 @@ +/* + * 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.cassandra; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.internal.builders.AllDefaultPossibilitiesBuilder; +import org.junit.runner.Description; +import org.junit.runner.Runner; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.model.RunnerBuilder; + +/** + * This class comes useful when debugging flaky tests that will fail only when the full test suite is ran. It is + * intended for test failure investigation only. + *
+ * Decorate your class with the runner and iterations you want. Defaults to 10. + * Beware of tests that change singleton status as those won't work. + *
{@code
+ * @RunWith(RepeatableRunner.class)
+ * @RepeatableRunnerConfiguration(iterations = 2, runner = BlockJUnit4ClassRunner.class)
+ * public class MyTest
+ * {
+ * ...
+ * }
+ * }
+ */
+public class RepeatableRunner extends Runner
+{
+ private static final int DEFAULT_REPETITIONS = 10;
+ private static final Class extends Runner> DEFAULT_RUNNER_CLASS = BlockJUnit4ClassRunner.class;
+
+ private final Runner runner;
+ private final int iterations;
+
+ public RepeatableRunner(Class> testClass) throws Throwable
+ {
+ Class extends Runner> runnerClass = DEFAULT_RUNNER_CLASS;
+
+ if (testClass.isAnnotationPresent(RepeatableRunnerConfiguration.class))
+ {
+ RepeatableRunnerConfiguration configuration = testClass.getAnnotation(RepeatableRunnerConfiguration.class);
+ iterations = configuration.iterations();
+ runnerClass = configuration.runner();
+ }
+ else
+ {
+ iterations = DEFAULT_REPETITIONS;
+ }
+
+ runner = buildRunner(runnerClass, testClass);
+ }
+
+ @Override
+ public Description getDescription()
+ {
+ return runner.getDescription();
+ }
+
+ @Override
+ public void run(RunNotifier notifier)
+ {
+ for (int i = 0; i < iterations; i++)
+ {
+ runner.run(notifier);
+ }
+ }
+
+ private static Runner buildRunner(Class extends Runner> runnerClass, Class> testClass) throws Throwable
+ {
+ try
+ {
+ return runnerClass.getConstructor(Class.class).newInstance(testClass);
+ }
+ catch (NoSuchMethodException e)
+ {
+ return runnerClass.getConstructor(Class.class, RunnerBuilder.class)
+ .newInstance(testClass, new AllDefaultPossibilitiesBuilder(true));
+ }
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ public @interface RepeatableRunnerConfiguration
+ {
+ int iterations() default DEFAULT_REPETITIONS;
+
+ Class extends Runner> runner() default BlockJUnit4ClassRunner.class;
+ }
+}