diff --git a/README.adoc b/README.adoc
index f75c6fa3..172c3f3d 100644
--- a/README.adoc
+++ b/README.adoc
@@ -57,7 +57,9 @@ The `retry` extension is of the following type:
----
package org.gradle.testretry;
+import org.gradle.api.Action;
import org.gradle.api.provider.Property;
+import org.gradle.api.provider.SetProperty;
import org.gradle.api.tasks.testing.Test;
/**
@@ -67,6 +69,11 @@ import org.gradle.api.tasks.testing.Test;
*/
public interface TestRetryTaskExtension {
+ /**
+ * The name of the extension added to each test task.
+ */
+ String NAME = "retry";
+
/**
* Whether tests that initially fail and then pass on retry should fail the task.
*
@@ -75,7 +82,7 @@ public interface TestRetryTaskExtension {
*
* This setting has no effect if {@link Test#getIgnoreFailures()} is set to true.
*
- * @return whether tests that initially fail and then pass on retry should fail the task
+ * @return whether tests that initially fails and then pass on retry should fail the task
*/
Property getFailOnPassedAfterRetry();
@@ -104,6 +111,77 @@ public interface TestRetryTaskExtension {
*/
Property getMaxFailures();
+ /**
+ * The filter for specifying which tests may be retried.
+ */
+ Filter getFilter();
+
+ /**
+ * The filter for specifying which tests may be retried.
+ */
+ void filter(Action super Filter> action);
+
+ /**
+ * A filter for specifying which tests may be retried.
+ *
+ * By default, all tests are eligible for retrying.
+ */
+ interface Filter {
+
+ /**
+ * The patterns used to include tests based on their class name.
+ *
+ * The pattern string matches against qualified class names.
+ * It may contain '*' characters, which match zero or more of any character.
+ *
+ * A class name only has to match one pattern to be included.
+ *
+ * If no patterns are specified, all classes (that also meet other configured filters) will be included.
+ */
+ SetProperty getIncludeClasses();
+
+ /**
+ * The patterns used to include tests based on their class level annotations.
+ *
+ * The pattern string matches against the qualified class names of a test class's annotations.
+ * It may contain '*' characters, which match zero or more of any character.
+ *
+ * A class need only have one annotation matching any of the patterns to be included.
+ *
+ * Annotations present on super classes that are {@code @Inherited} are considered when inspecting subclasses.
+ *
+ * If no patterns are specified, all classes (that also meet other configured filters) will be included.
+ */
+ SetProperty getIncludeAnnotationClasses();
+
+ /**
+ * The patterns used to exclude tests based on their class name.
+ *
+ * The pattern string matches against qualified class names.
+ * It may contain '*' characters, which match zero or more of any character.
+ *
+ * A class name only has to match one pattern to be excluded.
+ *
+ * If no patterns are specified, all classes (that also meet other configured filters) will be included.
+ */
+ SetProperty getExcludeClasses();
+
+ /**
+ * The patterns used to exclude tests based on their class level annotations.
+ *
+ * The pattern string matches against the qualified class names of a test class's annotations.
+ * It may contain '*' characters, which match zero or more of any character.
+ *
+ * A class need only have one annotation matching any of the patterns to be excluded.
+ *
+ * Annotations present on super classes that are {@code @Inherited} are considered when inspecting subclasses.
+ *
+ * If no patterns are specified, all classes (that also meet other configured filters) will be included.
+ */
+ SetProperty getExcludeAnnotationClasses();
+
+ }
+
}
----
@@ -143,6 +221,34 @@ The plugin supports retrying Spock `@Stepwise` tests and TestNG `@Test(dependsOn
* Upstream tests (those that the failed test depends on) are run because a flaky test may depend on state from the prior execution of an upstream test.
* Downstream tests are run because a flaky test causes any downstream tests to be skipped in the initial test run.
+== Filtering
+
+By default, all tests are eligible for retrying.
+The `filter` component of the test retry extension can be used to control which tests should be retried and which should not.
+
+The decision to retry a test or not is based on the tests reported class name, regardless of the name of the test case or method.
+The annotations present or not on this class can also be used as the criteria.
+
+.build.gradle:
+[source,groovy]
+----
+test {
+ retry {
+ maxRetries = 3
+ filter {
+ // filter by qualified class name (* matches zero or more of any character)
+ includeClasses.add("*IntegrationTest")
+ excludeClasses.add("*DatabaseTest")
+
+ // filter by class level annotations
+ // Note: @Inherited annotations are respected
+ includeAnnotationClasses.add("*Retryable")
+ excludeAnnotationClasses.add("*NonRetryable")
+ }
+ }
+}
+----
+
== Reporting
=== Gradle
diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts
index 0f7c9e9e..17384f74 100644
--- a/plugin/build.gradle.kts
+++ b/plugin/build.gradle.kts
@@ -40,6 +40,7 @@ dependencies {
testImplementation(localGroovy())
testImplementation("org.spockframework:spock-core:1.3-groovy-2.5")
testImplementation("net.sourceforge.nekohtml:nekohtml:1.9.22")
+ testImplementation("org.ow2.asm:asm:8.0.1")
codenarc("org.codenarc:CodeNarc:1.0")
}
diff --git a/plugin/src/main/java/org/gradle/testretry/TestRetryTaskExtension.java b/plugin/src/main/java/org/gradle/testretry/TestRetryTaskExtension.java
index a05be7f9..46acdaf6 100644
--- a/plugin/src/main/java/org/gradle/testretry/TestRetryTaskExtension.java
+++ b/plugin/src/main/java/org/gradle/testretry/TestRetryTaskExtension.java
@@ -15,7 +15,9 @@
*/
package org.gradle.testretry;
+import org.gradle.api.Action;
import org.gradle.api.provider.Property;
+import org.gradle.api.provider.SetProperty;
import org.gradle.api.tasks.testing.Test;
/**
@@ -67,4 +69,75 @@ public interface TestRetryTaskExtension {
*/
Property getMaxFailures();
+ /**
+ * The filter for specifying which tests may be retried.
+ */
+ Filter getFilter();
+
+ /**
+ * The filter for specifying which tests may be retried.
+ */
+ void filter(Action super Filter> action);
+
+ /**
+ * A filter for specifying which tests may be retried.
+ *
+ * By default, all tests are eligible for retrying.
+ */
+ interface Filter {
+
+ /**
+ * The patterns used to include tests based on their class name.
+ *
+ * The pattern string matches against qualified class names.
+ * It may contain '*' characters, which match zero or more of any character.
+ *
+ * A class name only has to match one pattern to be included.
+ *
+ * If no patterns are specified, all classes (that also meet other configured filters) will be included.
+ */
+ SetProperty getIncludeClasses();
+
+ /**
+ * The patterns used to include tests based on their class level annotations.
+ *
+ * The pattern string matches against the qualified class names of a test class's annotations.
+ * It may contain '*' characters, which match zero or more of any character.
+ *
+ * A class need only have one annotation matching any of the patterns to be included.
+ *
+ * Annotations present on super classes that are {@code @Inherited} are considered when inspecting subclasses.
+ *
+ * If no patterns are specified, all classes (that also meet other configured filters) will be included.
+ */
+ SetProperty getIncludeAnnotationClasses();
+
+ /**
+ * The patterns used to exclude tests based on their class name.
+ *
+ * The pattern string matches against qualified class names.
+ * It may contain '*' characters, which match zero or more of any character.
+ *
+ * A class name only has to match one pattern to be excluded.
+ *
+ * If no patterns are specified, all classes (that also meet other configured filters) will be included.
+ */
+ SetProperty getExcludeClasses();
+
+ /**
+ * The patterns used to exclude tests based on their class level annotations.
+ *
+ * The pattern string matches against the qualified class names of a test class's annotations.
+ * It may contain '*' characters, which match zero or more of any character.
+ *
+ * A class need only have one annotation matching any of the patterns to be excluded.
+ *
+ * Annotations present on super classes that are {@code @Inherited} are considered when inspecting subclasses.
+ *
+ * If no patterns are specified, all classes (that also meet other configured filters) will be included.
+ */
+ SetProperty getExcludeAnnotationClasses();
+
+ }
+
}
diff --git a/plugin/src/main/java/org/gradle/testretry/internal/config/DefaultTestRetryTaskExtension.java b/plugin/src/main/java/org/gradle/testretry/internal/config/DefaultTestRetryTaskExtension.java
index 582e3717..3dc0eef8 100644
--- a/plugin/src/main/java/org/gradle/testretry/internal/config/DefaultTestRetryTaskExtension.java
+++ b/plugin/src/main/java/org/gradle/testretry/internal/config/DefaultTestRetryTaskExtension.java
@@ -15,8 +15,10 @@
*/
package org.gradle.testretry.internal.config;
+import org.gradle.api.Action;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.Property;
+import org.gradle.api.provider.SetProperty;
import org.gradle.testretry.TestRetryTaskExtension;
import javax.inject.Inject;
@@ -26,12 +28,14 @@ public class DefaultTestRetryTaskExtension implements TestRetryTaskExtension {
private final Property failOnPassedAfterRetry;
private final Property maxRetries;
private final Property maxFailures;
+ private final Filter filter;
@Inject
public DefaultTestRetryTaskExtension(ObjectFactory objects) {
this.failOnPassedAfterRetry = objects.property(Boolean.class);
this.maxRetries = objects.property(Integer.class);
this.maxFailures = objects.property(Integer.class);
+ this.filter = new FilterImpl(objects);
}
public Property getFailOnPassedAfterRetry() {
@@ -46,4 +50,49 @@ public Property getMaxFailures() {
return maxFailures;
}
+ @Override
+ public void filter(Action super Filter> action) {
+ action.execute(filter);
+ }
+
+ @Override
+ public Filter getFilter() {
+ return filter;
+ }
+
+ private static final class FilterImpl implements Filter {
+
+ private final SetProperty includeClasses;
+ private final SetProperty includeAnnotationClasses;
+ private final SetProperty excludeClasses;
+ private final SetProperty excludeAnnotationClasses;
+
+ public FilterImpl(ObjectFactory objects) {
+ this.includeClasses = objects.setProperty(String.class);
+ this.includeAnnotationClasses = objects.setProperty(String.class);
+ this.excludeClasses = objects.setProperty(String.class);
+ this.excludeAnnotationClasses = objects.setProperty(String.class);
+ }
+
+ @Override
+ public SetProperty getIncludeClasses() {
+ return includeClasses;
+ }
+
+ @Override
+ public SetProperty getIncludeAnnotationClasses() {
+ return includeAnnotationClasses;
+ }
+
+ @Override
+ public SetProperty getExcludeClasses() {
+ return excludeClasses;
+ }
+
+ @Override
+ public SetProperty getExcludeAnnotationClasses() {
+ return excludeAnnotationClasses;
+ }
+ }
+
}
diff --git a/plugin/src/main/java/org/gradle/testretry/internal/config/TestRetryTaskExtensionAdapter.java b/plugin/src/main/java/org/gradle/testretry/internal/config/TestRetryTaskExtensionAdapter.java
index cbe246b9..c3976c8a 100644
--- a/plugin/src/main/java/org/gradle/testretry/internal/config/TestRetryTaskExtensionAdapter.java
+++ b/plugin/src/main/java/org/gradle/testretry/internal/config/TestRetryTaskExtensionAdapter.java
@@ -18,9 +18,12 @@
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.provider.ProviderFactory;
+import org.gradle.api.provider.SetProperty;
import org.gradle.testretry.TestRetryTaskExtension;
-import org.jetbrains.annotations.NotNull;
+import org.gradle.util.VersionNumber;
+import java.util.Collections;
+import java.util.Set;
import java.util.concurrent.Callable;
public final class TestRetryTaskExtensionAdapter {
@@ -34,29 +37,44 @@ public final class TestRetryTaskExtensionAdapter {
private final ProviderFactory providerFactory;
private final TestRetryTaskExtension extension;
- private final boolean useConventions;
private final boolean simulateNotRetryableTest;
+ private boolean useConventions;
public TestRetryTaskExtensionAdapter(
ProviderFactory providerFactory,
TestRetryTaskExtension extension,
- boolean useConventions
+ VersionNumber gradleVersion
+
) {
this.providerFactory = providerFactory;
this.extension = extension;
- this.useConventions = useConventions;
+ this.simulateNotRetryableTest = Boolean.getBoolean(SIMULATE_NOT_RETRYABLE_PROPERTY);
- simulateNotRetryableTest = Boolean.getBoolean(SIMULATE_NOT_RETRYABLE_PROPERTY);
+ boolean gradle51OrLater = gradleVersion.getMajor() == 5
+ ? gradleVersion.getMinor() >= 1
+ : gradleVersion.getMajor() > 5;
- if (useConventions) {
- setDefaults(extension);
- }
+ this.useConventions = gradle51OrLater;
+
+ initialize(extension, gradle51OrLater);
}
- private void setDefaults(TestRetryTaskExtension extension) {
- extension.getMaxRetries().convention(DEFAULT_MAX_RETRIES);
- extension.getMaxFailures().convention(DEFAULT_MAX_FAILURES);
- extension.getFailOnPassedAfterRetry().convention(DEFAULT_FAIL_ON_PASSED_AFTER_RETRY);
+ private void initialize(TestRetryTaskExtension extension, boolean gradle51OrLater) {
+ if (gradle51OrLater) {
+ extension.getMaxRetries().convention(DEFAULT_MAX_RETRIES);
+ extension.getMaxFailures().convention(DEFAULT_MAX_FAILURES);
+ extension.getFailOnPassedAfterRetry().convention(DEFAULT_FAIL_ON_PASSED_AFTER_RETRY);
+ extension.getFilter().getIncludeClasses().convention(Collections.emptySet());
+ extension.getFilter().getIncludeAnnotationClasses().convention(Collections.emptySet());
+ extension.getFilter().getExcludeClasses().convention(Collections.emptySet());
+ extension.getFilter().getExcludeAnnotationClasses().convention(Collections.emptySet());
+ } else {
+ // https://github.com/gradle/gradle/issues/7485
+ extension.getFilter().getIncludeClasses().empty();
+ extension.getFilter().getIncludeAnnotationClasses().empty();
+ extension.getFilter().getExcludeClasses().empty();
+ extension.getFilter().getExcludeAnnotationClasses().empty();
+ }
}
Callable> getFailOnPassedAfterRetryInput() {
@@ -85,12 +103,32 @@ public int getMaxFailures() {
return read(extension.getMaxFailures(), DEFAULT_MAX_FAILURES);
}
+ public Set getIncludeClasses() {
+ return read(extension.getFilter().getIncludeClasses(), Collections.emptySet());
+ }
+
+ public Set getIncludeAnnotationClasses() {
+ return read(extension.getFilter().getIncludeAnnotationClasses(), Collections.emptySet());
+ }
+
+ public Set getExcludeClasses() {
+ return read(extension.getFilter().getExcludeClasses(), Collections.emptySet());
+ }
+
+ public Set getExcludeAnnotationClasses() {
+ return read(extension.getFilter().getExcludeAnnotationClasses(), Collections.emptySet());
+ }
+
public boolean getSimulateNotRetryableTest() {
return simulateNotRetryableTest;
}
- @NotNull
private T read(Property property, T defaultValue) {
return useConventions ? property.get() : property.getOrElse(defaultValue);
}
+
+ private Set read(SetProperty property, Set defaultValue) {
+ return useConventions ? property.get() : property.getOrElse(defaultValue);
+ }
+
}
diff --git a/plugin/src/main/java/org/gradle/testretry/internal/config/TestTaskConfigurer.java b/plugin/src/main/java/org/gradle/testretry/internal/config/TestTaskConfigurer.java
index ed895398..81cc5b02 100644
--- a/plugin/src/main/java/org/gradle/testretry/internal/config/TestTaskConfigurer.java
+++ b/plugin/src/main/java/org/gradle/testretry/internal/config/TestTaskConfigurer.java
@@ -40,14 +40,9 @@ private TestTaskConfigurer() {
public static void configureTestTask(Test test, ObjectFactory objectFactory, ProviderFactory providerFactory) {
VersionNumber gradleVersion = VersionNumber.parse(test.getProject().getGradle().getGradleVersion());
- TestRetryTaskExtension extension;
- if (supportsGeneratedAbstractTypeImplementations(gradleVersion)) {
- extension = objectFactory.newInstance(TestRetryTaskExtension.class);
- } else {
- extension = objectFactory.newInstance(DefaultTestRetryTaskExtension.class);
- }
+ TestRetryTaskExtension extension = objectFactory.newInstance(DefaultTestRetryTaskExtension.class);
- TestRetryTaskExtensionAdapter adapter = new TestRetryTaskExtensionAdapter(providerFactory, extension, supportsPropertyConventions(gradleVersion));
+ TestRetryTaskExtensionAdapter adapter = new TestRetryTaskExtensionAdapter(providerFactory, extension, gradleVersion);
test.getInputs().property("retry.failOnPassedAfterRetry", adapter.getFailOnPassedAfterRetryInput());
@@ -70,10 +65,6 @@ private static void setTestExecuter(Test task, RetryTestExecuter retryTestExecut
invoke(declaredMethod(Test.class, "setTestExecuter", TestExecuter.class), task, retryTestExecuter);
}
- private static boolean supportsGeneratedAbstractTypeImplementations(VersionNumber gradleVersion) {
- return gradleVersion.getMajor() == 5 ? gradleVersion.getMinor() >= 3 : gradleVersion.getMajor() > 5;
- }
-
private static boolean supportsPropertyConventions(VersionNumber gradleVersion) {
return gradleVersion.getMajor() == 5 ? gradleVersion.getMinor() >= 1 : gradleVersion.getMajor() > 5;
}
diff --git a/plugin/src/main/java/org/gradle/testretry/internal/executer/RetryTestExecuter.java b/plugin/src/main/java/org/gradle/testretry/internal/executer/RetryTestExecuter.java
index f37888cc..36244039 100644
--- a/plugin/src/main/java/org/gradle/testretry/internal/executer/RetryTestExecuter.java
+++ b/plugin/src/main/java/org/gradle/testretry/internal/executer/RetryTestExecuter.java
@@ -24,6 +24,8 @@
import org.gradle.internal.reflect.Instantiator;
import org.gradle.testretry.internal.config.TestRetryTaskExtensionAdapter;
import org.gradle.testretry.internal.executer.framework.TestFrameworkStrategy;
+import org.gradle.testretry.internal.filter.AnnotationInspectorImpl;
+import org.gradle.testretry.internal.filter.RetryFilter;
import java.util.stream.Collectors;
@@ -66,8 +68,17 @@ public void execute(JvmTestExecutionSpec spec, TestResultProcessor testResultPro
TestFrameworkStrategy testFrameworkStrategy = TestFrameworkStrategy.of(spec.getTestFramework());
+ RetryFilter filter = new RetryFilter(
+ new AnnotationInspectorImpl(frameworkTemplate.testsReader),
+ extension.getIncludeClasses(),
+ extension.getIncludeAnnotationClasses(),
+ extension.getExcludeClasses(),
+ extension.getExcludeAnnotationClasses()
+ );
+
RetryTestResultProcessor retryTestResultProcessor = new RetryTestResultProcessor(
testFrameworkStrategy,
+ filter,
frameworkTemplate.testsReader,
testResultProcessor,
maxFailures
@@ -86,7 +97,7 @@ public void execute(JvmTestExecutionSpec spec, TestResultProcessor testResultPro
testTask.setIgnoreFailures(true);
break;
} else if (result.failedTests.isEmpty()) {
- if (retryCount > 0 && !failOnPassedAfterRetry) {
+ if (retryCount > 0 && !result.hasRetryFilteredFailures && !failOnPassedAfterRetry) {
testTask.setIgnoreFailures(true);
}
break;
diff --git a/plugin/src/main/java/org/gradle/testretry/internal/executer/RetryTestResultProcessor.java b/plugin/src/main/java/org/gradle/testretry/internal/executer/RetryTestResultProcessor.java
index c6829cb3..6170b555 100644
--- a/plugin/src/main/java/org/gradle/testretry/internal/executer/RetryTestResultProcessor.java
+++ b/plugin/src/main/java/org/gradle/testretry/internal/executer/RetryTestResultProcessor.java
@@ -21,6 +21,8 @@
import org.gradle.api.internal.tasks.testing.TestStartEvent;
import org.gradle.api.tasks.testing.TestOutputEvent;
import org.gradle.testretry.internal.executer.framework.TestFrameworkStrategy;
+import org.gradle.testretry.internal.filter.RetryFilter;
+import org.gradle.testretry.internal.testsreader.TestsReader;
import java.util.HashMap;
import java.util.Map;
@@ -30,11 +32,13 @@
final class RetryTestResultProcessor implements TestResultProcessor {
private final TestFrameworkStrategy testFrameworkStrategy;
+ private final RetryFilter filter;
private final TestsReader testsReader;
private final TestResultProcessor delegate;
private final int maxFailures;
private boolean lastRetry;
+ private boolean hasRetryFilteredFailures;
private final Map