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 DEFAULT_RUNNER_CLASS = BlockJUnit4ClassRunner.class; + + private final Runner runner; + private final int iterations; + + public RepeatableRunner(Class testClass) throws Throwable + { + Class 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 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 runner() default BlockJUnit4ClassRunner.class; + } +}