Skip to content

Commit f3bc377

Browse files
committed
Ability to retry a data provider during failures
Closes #2819
1 parent 8b70f6f commit f3bc377

16 files changed

+411
-103
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
Current
2+
Fixed: GITHUB-2819: Ability to retry a data provider in case of failures (Krishnan Mahadevan)
23
Fixed: GITHUB-2308: StringIndexOutOfBoundsException in findClassesInPackage - Surefire/Maven - JDK 11 fails (Krishnan Mahadevan)
34
Fixed: GITHUB:2788: TestResult.isSuccess() is TRUE when test fails due to expectedExceptions (Krishnan Mahadevan)
45
Fixed: GITHUB-2800: Running Test Classes with Inherited @Factory and @DataProvider Annotated Non-Static Methods Fail (Krishnan Mahadevan)

testng-core-api/src/main/java/org/testng/IDataProviderMethod.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,12 @@ public interface IDataProviderMethod {
3030
default boolean propagateFailureAsTestFailure() {
3131
return false;
3232
}
33+
34+
/**
35+
* @return - An Class which implements {@link IRetryDataProvider} and which can be used to retry a
36+
* data provider.
37+
*/
38+
default Class<? extends IRetryDataProvider> retryUsing() {
39+
return IRetryDataProvider.DisableDataProviderRetries.class;
40+
}
3341
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.testng;
2+
3+
/** Represents the ability to retry a data provider. */
4+
public interface IRetryDataProvider {
5+
6+
/**
7+
* @param dataProvider - The {@link IDataProviderMethod} object which represents the data provider
8+
* to be invoked.
9+
* @return - <code>true</code> if the data provider should be invoked again.
10+
*/
11+
boolean retry(IDataProviderMethod dataProvider);
12+
13+
/** A dummy implementation which disables retrying of a failed data provider. */
14+
class DisableDataProviderRetries implements IRetryDataProvider {
15+
16+
@Override
17+
public boolean retry(IDataProviderMethod dataProvider) {
18+
return false;
19+
}
20+
}
21+
}

testng-core-api/src/main/java/org/testng/annotations/DataProvider.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.lang.annotation.Documented;
66
import java.lang.annotation.Retention;
77
import java.lang.annotation.Target;
8+
import org.testng.IRetryDataProvider;
89

910
/**
1011
* Mark a method as supplying data for a test method.
@@ -55,4 +56,11 @@
5556
* @return the value
5657
*/
5758
boolean propagateFailureAsTestFailure() default false;
59+
60+
/**
61+
* @return - An Class which implements {@link IRetryDataProvider} and which can be used to retry a
62+
* data provider.
63+
*/
64+
Class<? extends IRetryDataProvider> retryUsing() default
65+
IRetryDataProvider.DisableDataProviderRetries.class;
5866
}

testng-core-api/src/main/java/org/testng/annotations/IDataProviderAnnotation.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.testng.annotations;
22

33
import java.util.List;
4+
import org.testng.IRetryDataProvider;
45

56
/** Encapsulate the @DataProvider / @testng.data-provider annotation */
67
public interface IDataProviderAnnotation extends IAnnotation {
@@ -27,4 +28,16 @@ public interface IDataProviderAnnotation extends IAnnotation {
2728

2829
/** @return - <code>true</code>If data provider failures should be propagated as test failures */
2930
boolean isPropagateFailureAsTestFailure();
31+
32+
/**
33+
* @param retry - A Class that implements {@link IRetryDataProvider} and which can be used to
34+
* retry a data provider.
35+
*/
36+
void setRetryUsing(Class<? extends IRetryDataProvider> retry);
37+
38+
/**
39+
* @return - An Class which implements {@link IRetryDataProvider} and which can be used to retry a
40+
* data provider.
41+
*/
42+
Class<? extends IRetryDataProvider> retryUsing();
3043
}

testng-core/src/main/java/org/testng/internal/DataProviderMethod.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.lang.reflect.Method;
44
import java.util.List;
55
import org.testng.IDataProviderMethod;
6+
import org.testng.IRetryDataProvider;
67
import org.testng.annotations.IDataProviderAnnotation;
78

89
/** Represents an @{@link org.testng.annotations.DataProvider} annotated method. */
@@ -47,4 +48,9 @@ public List<Integer> getIndices() {
4748
public boolean propagateFailureAsTestFailure() {
4849
return annotation.isPropagateFailureAsTestFailure();
4950
}
51+
52+
@Override
53+
public Class<? extends IRetryDataProvider> retryUsing() {
54+
return annotation.retryUsing();
55+
}
5056
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.testng.internal;
2+
3+
import java.util.Iterator;
4+
import java.util.List;
5+
import org.testng.ITestNGMethod;
6+
import org.testng.TestNGException;
7+
8+
class FilteredParameters implements Iterator<Object[]> {
9+
10+
private int index = 0;
11+
private boolean hasWarn = false;
12+
private final Iterator<Object[]> parameters;
13+
private final ITestNGMethod testMethod;
14+
private final String dataProviderName;
15+
private final List<Integer> indices;
16+
17+
public FilteredParameters(
18+
Iterator<Object[]> parameters,
19+
ITestNGMethod testMethod,
20+
String dataProviderName,
21+
List<Integer> indices) {
22+
this.parameters = parameters;
23+
this.testMethod = testMethod;
24+
this.dataProviderName = dataProviderName;
25+
this.indices = indices;
26+
}
27+
28+
@Override
29+
public boolean hasNext() {
30+
if (index == 0 && !parameters.hasNext() && !hasWarn) {
31+
hasWarn = true;
32+
String msg =
33+
String.format(
34+
"The test method '%s' will be skipped since its "
35+
+ "data provider '%s' "
36+
+ "returned an empty array or iterator. ",
37+
testMethod.getQualifiedName(), dataProviderName);
38+
Utils.warn(msg);
39+
}
40+
return parameters.hasNext();
41+
}
42+
43+
@Override
44+
public Object[] next() {
45+
testMethod.setParameterInvocationCount(index);
46+
Object[] next = parameters.next();
47+
if (next == null) {
48+
throw new TestNGException("Parameters must not be null");
49+
}
50+
if (!indices.isEmpty() && !indices.contains(index)) {
51+
// Skip parameters
52+
next = null;
53+
}
54+
index++;
55+
return next;
56+
}
57+
58+
@Override
59+
public void remove() {
60+
throw new UnsupportedOperationException("remove");
61+
}
62+
}

testng-core/src/main/java/org/testng/internal/Parameters.java

Lines changed: 63 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
import java.lang.reflect.Modifier;
77
import java.lang.reflect.Parameter;
88
import java.util.*;
9-
import javax.annotation.Nullable;
109
import org.testng.DataProviderHolder;
1110
import org.testng.IDataProviderInterceptor;
1211
import org.testng.IDataProviderListener;
1312
import org.testng.IDataProviderMethod;
13+
import org.testng.IRetryDataProvider;
1414
import org.testng.ITestClass;
1515
import org.testng.ITestContext;
1616
import org.testng.ITestNGMethod;
@@ -147,7 +147,7 @@ public static Object[] createConfigurationParameters(
147147
Method m,
148148
Map<String, String> params,
149149
Object[] parameterValues,
150-
@Nullable ITestNGMethod currentTestMethod,
150+
ITestNGMethod currentTestMethod,
151151
IAnnotationFinder finder,
152152
XmlSuite xmlSuite,
153153
ITestContext ctx,
@@ -618,34 +618,33 @@ private static IDataProviderMethod findDataProvider(
618618

619619
for (Method m : ClassHelper.getAvailableMethods(cls)) {
620620
IDataProviderAnnotation dp = finder.findAnnotation(m, IDataProviderAnnotation.class);
621-
if (null != dp && name.equals(getDataProviderName(dp, m))) {
622-
Object instanceToUse;
623-
if (shouldBeStatic && (m.getModifiers() & Modifier.STATIC) == 0) {
624-
IObjectDispenser dispenser = Dispenser.newInstance(objectFactory);
625-
BasicAttributes basic = new BasicAttributes(clazz, dataProviderClass);
626-
CreationAttributes attributes = new CreationAttributes(context, basic, null);
627-
instanceToUse = dispenser.dispense(attributes);
628-
} else {
629-
instanceToUse = instance;
630-
}
631-
// Not a static method but no instance exists, then create new one if possible
632-
if ((m.getModifiers() & Modifier.STATIC) == 0 && instanceToUse == null) {
633-
try {
634-
instanceToUse = objectFactory.newInstance(cls);
635-
} catch (TestNGException e) {
636-
instanceToUse = null;
637-
}
621+
boolean proceed = null != dp && name.equals(getDataProviderName(dp, m));
622+
if (!proceed) {
623+
continue;
624+
}
625+
Object instanceToUse = instance;
626+
if (shouldBeStatic && (m.getModifiers() & Modifier.STATIC) == 0) {
627+
IObjectDispenser dispenser = Dispenser.newInstance(objectFactory);
628+
BasicAttributes basic = new BasicAttributes(clazz, dataProviderClass);
629+
CreationAttributes attributes = new CreationAttributes(context, basic, null);
630+
instanceToUse = dispenser.dispense(attributes);
631+
}
632+
// Not a static method but no instance exists, then create new one if possible
633+
if ((m.getModifiers() & Modifier.STATIC) == 0 && instanceToUse == null) {
634+
try {
635+
instanceToUse = objectFactory.newInstance(cls);
636+
} catch (TestNGException ignored) {
638637
}
638+
}
639639

640-
if (result != null) {
641-
throw new TestNGException("Found two providers called '" + name + "' on " + cls);
642-
}
640+
if (result != null) {
641+
throw new TestNGException("Found two providers called '" + name + "' on " + cls);
642+
}
643643

644-
if (isDynamicDataProvider) {
645-
result = new DataProviderMethodRemovable(instanceToUse, m, dp);
646-
} else {
647-
result = new DataProviderMethod(instanceToUse, m, dp);
648-
}
644+
if (isDynamicDataProvider) {
645+
result = new DataProviderMethodRemovable(instanceToUse, m, dp);
646+
} else {
647+
result = new DataProviderMethod(instanceToUse, m, dp);
649648
}
650649
}
651650

@@ -780,31 +779,46 @@ public static ParameterHolder handleParameters(
780779
String n = "param" + i;
781780
allParameterNames.put(n, n);
782781
}
783-
784-
for (IDataProviderListener dataProviderListener : holder.getListeners()) {
785-
dataProviderListener.beforeDataProviderExecution(
786-
dataProviderMethod, testMethod, methodParams.context);
782+
Class<?> retryClass = dataProviderMethod.retryUsing();
783+
boolean shouldRetry = !retryClass.equals(IRetryDataProvider.DisableDataProviderRetries.class);
784+
IRetryDataProvider retry = null;
785+
if (shouldRetry) {
786+
IObjectDispenser dispenser = Dispenser.newInstance(objectFactory);
787+
BasicAttributes basic = new BasicAttributes(testMethod.getTestClass(), retryClass);
788+
CreationAttributes attributes = new CreationAttributes(methodParams.context, basic, null);
789+
retry = (IRetryDataProvider) dispenser.dispense(attributes);
787790
}
788791

789-
Iterator<Object[]> initParams;
790-
try {
791-
initParams =
792-
MethodInvocationHelper.invokeDataProvider(
793-
dataProviderMethod
794-
.getInstance(), /* a test instance or null if the data provider is static*/
795-
dataProviderMethod.getMethod(),
796-
testMethod,
797-
methodParams.context,
798-
fedInstance,
799-
annotationFinder);
800-
} catch (RuntimeException e) {
801-
for (IDataProviderListener each : holder.getListeners()) {
802-
each.onDataProviderFailure(testMethod, methodParams.context, e);
792+
Iterator<Object[]> initParams = null;
793+
do {
794+
795+
for (IDataProviderListener dataProviderListener : holder.getListeners()) {
796+
dataProviderListener.beforeDataProviderExecution(
797+
dataProviderMethod, testMethod, methodParams.context);
803798
}
804-
throw e;
805-
}
806799

807-
final Iterator<Object[]> parameters = initParams;
800+
try {
801+
initParams =
802+
MethodInvocationHelper.invokeDataProvider(
803+
dataProviderMethod
804+
.getInstance(), /* a test instance or null if the data provider is static*/
805+
dataProviderMethod.getMethod(),
806+
testMethod,
807+
methodParams.context,
808+
fedInstance,
809+
annotationFinder);
810+
shouldRetry = false;
811+
} catch (RuntimeException e) {
812+
for (IDataProviderListener each : holder.getListeners()) {
813+
each.onDataProviderFailure(testMethod, methodParams.context, e);
814+
}
815+
if (shouldRetry) {
816+
shouldRetry = retry.retry(dataProviderMethod);
817+
} else {
818+
throw e;
819+
}
820+
}
821+
} while (shouldRetry);
808822

809823
for (IDataProviderListener dataProviderListener : holder.getListeners()) {
810824
dataProviderListener.afterDataProviderExecution(
@@ -817,45 +831,7 @@ public static ParameterHolder handleParameters(
817831
allIndices.addAll(dataProviderMethod.getIndices());
818832

819833
Iterator<Object[]> filteredParameters =
820-
new Iterator<Object[]>() {
821-
int index = 0;
822-
boolean hasWarn = false;
823-
824-
@Override
825-
public boolean hasNext() {
826-
if (index == 0 && !parameters.hasNext() && !hasWarn) {
827-
hasWarn = true;
828-
String msg =
829-
String.format(
830-
"The test method '%s' will be skipped since its "
831-
+ "data provider '%s' "
832-
+ "returned an empty array or iterator. ",
833-
testMethod.getQualifiedName(), dataProviderMethod.getName());
834-
Utils.warn(msg);
835-
}
836-
return parameters.hasNext();
837-
}
838-
839-
@Override
840-
public Object[] next() {
841-
testMethod.setParameterInvocationCount(index);
842-
Object[] next = parameters.next();
843-
if (next == null) {
844-
throw new TestNGException("Parameters must not be null");
845-
}
846-
if (!allIndices.isEmpty() && !allIndices.contains(index)) {
847-
// Skip parameters
848-
next = null;
849-
}
850-
index++;
851-
return next;
852-
}
853-
854-
@Override
855-
public void remove() {
856-
throw new UnsupportedOperationException("remove");
857-
}
858-
};
834+
new FilteredParameters(initParams, testMethod, dataProviderMethod.getName(), allIndices);
859835

860836
testMethod.setMoreInvocationChecker(filteredParameters::hasNext);
861837
for (IDataProviderInterceptor interceptor : holder.getInterceptors()) {
@@ -919,18 +895,6 @@ public static Object[] injectParameters(
919895
return matcher.getConformingArguments();
920896
}
921897

922-
public static Object[] getParametersFromIndex(Iterator<Object[]> parametersValues, int index) {
923-
while (parametersValues.hasNext()) {
924-
Object[] parameters = parametersValues.next();
925-
926-
if (index == 0) {
927-
return parameters;
928-
}
929-
index--;
930-
}
931-
return null;
932-
}
933-
934898
/** A parameter passing helper class. */
935899
public static class MethodParameters {
936900
private final Map<String, String> xmlParameters;

0 commit comments

Comments
 (0)