diff --git a/build.gradle.kts b/build.gradle.kts
index 995465377a..10456e3162 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -68,7 +68,7 @@ dependencies {
}
listOf("com.beust:jcommander:1.72", "com.google.inject:guice:4.1.0:no_aop",
- "org.yaml:snakeyaml:1.21").forEach {
+ "org.yaml:snakeyaml:1.21", "net.bytebuddy:byte-buddy:1.10.1").forEach {
compile(it)
}
diff --git a/src/main/java/org/testng/annotations/ParameterCollector.java b/src/main/java/org/testng/annotations/ParameterCollector.java
new file mode 100644
index 0000000000..33fe86ae2d
--- /dev/null
+++ b/src/main/java/org/testng/annotations/ParameterCollector.java
@@ -0,0 +1,16 @@
+package org.testng.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.PARAMETER;
+
+/**
+ * If this annotation is used on a parameter of a data provider, that parameter is the proxy to the
+ * class which uses data provider. Call methods of that proxy to provide parameters for a test.
+ *
+ *
This annotation is ignored everywhere else.
+ */
+@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
+@Target({PARAMETER})
+public @interface ParameterCollector {}
diff --git a/src/main/java/org/testng/internal/MethodInvocationHelper.java b/src/main/java/org/testng/internal/MethodInvocationHelper.java
index 5b08910b05..25f7851646 100644
--- a/src/main/java/org/testng/internal/MethodInvocationHelper.java
+++ b/src/main/java/org/testng/internal/MethodInvocationHelper.java
@@ -1,6 +1,10 @@
package org.testng.internal;
import java.util.concurrent.TimeUnit;
+
+import net.bytebuddy.ByteBuddy;
+import net.bytebuddy.implementation.InvocationHandlerAdapter;
+import net.bytebuddy.matcher.ElementMatchers;
import org.testng.IConfigurable;
import org.testng.IConfigureCallBack;
import org.testng.IHookCallBack;
@@ -31,6 +35,7 @@
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
+import java.util.Objects;
/**
* Collections of helper methods to help deal with invocation of TestNG methods
@@ -141,11 +146,19 @@ protected static Iterator invokeDataProvider(
ITestContext testContext,
Object fedInstance,
IAnnotationFinder annotationFinder) {
+ List resultHolder = new ArrayList<>();
List parameters =
- getParameters(dataProvider, method, testContext, fedInstance, annotationFinder);
+ getParameters(dataProvider, method, testContext, fedInstance, resultHolder, annotationFinder);
Object result = invokeMethodNoCheckedException(dataProvider, instance, parameters);
if (result == null) {
+ if (!resultHolder.isEmpty()) {
+ return resultHolder.iterator();
+ }
throw new TestNGException("Data Provider " + dataProvider + " returned a null value");
+ } else {
+ if (!resultHolder.isEmpty()) {
+ throw new TestNGException("Data Provider " + dataProvider + " must return void if using @ParameterCollector");
+ }
}
// If it returns an Object[][] or Object[], convert it to an Iterator
if (result instanceof Object[][]) {
@@ -180,6 +193,7 @@ private static List getParameters(
ITestNGMethod method,
ITestContext testContext,
Object fedInstance,
+ List resultHolder,
IAnnotationFinder annotationFinder) {
// Go through all the parameters declared on this Data Provider and
// make sure we have at most one Method and one ITestContext.
@@ -203,8 +217,32 @@ private static List getParameters(
parameters.add(com.getDeclaringClass());
} else {
boolean isTestInstance = annotationFinder.hasTestInstance(dataProvider, i);
+ boolean isParameterCollector = annotationFinder.hasParameterCollector(dataProvider, i);
if (isTestInstance) {
parameters.add(fedInstance);
+ } else if (isParameterCollector) {
+ final String expectedMethodName = method.getMethodName();
+ Class> proxyClass = new ByteBuddy()
+ .subclass(fedInstance.getClass())
+ .method(ElementMatchers.any())
+ .intercept(InvocationHandlerAdapter.of((proxy, calledMethod, args) -> {
+ final String actualMethodName = calledMethod.getName();
+ if(!Objects.equals(expectedMethodName, actualMethodName)) {
+ throw new TestNGException(
+ String.format("Wrong method %s is called, expected %s",
+ actualMethodName, expectedMethodName));
+ }
+ resultHolder.add(args);
+ return null;
+ }))
+ .make()
+ .load(fedInstance.getClass().getClassLoader())
+ .getLoaded();
+ try {
+ parameters.add(proxyClass.newInstance());
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new TestNGException("Failed to proxy class", e);
+ }
} else {
unresolved.add(new Pair<>(i, cls));
}
diff --git a/src/main/java/org/testng/internal/annotations/IAnnotationFinder.java b/src/main/java/org/testng/internal/annotations/IAnnotationFinder.java
index ff8151a592..0447f06d5f 100644
--- a/src/main/java/org/testng/internal/annotations/IAnnotationFinder.java
+++ b/src/main/java/org/testng/internal/annotations/IAnnotationFinder.java
@@ -46,6 +46,10 @@ A findAnnotation(
/** @return true if the ith parameter of the given method has the annotation @TestInstance. */
boolean hasTestInstance(Method method, int i);
+ /** @return true if the ith parameter of the given method has the annotation
+ * {@link org.testng.annotations.ParameterCollector }. */
+ boolean hasParameterCollector(Method method, int i);
+
/**
* @return the @Optional values of this method's parameters (null
if the parameter
* isn't optional)
diff --git a/src/main/java/org/testng/internal/annotations/JDK15AnnotationFinder.java b/src/main/java/org/testng/internal/annotations/JDK15AnnotationFinder.java
index bf648f99b3..8c53e605ca 100644
--- a/src/main/java/org/testng/internal/annotations/JDK15AnnotationFinder.java
+++ b/src/main/java/org/testng/internal/annotations/JDK15AnnotationFinder.java
@@ -32,6 +32,7 @@
import org.testng.annotations.ObjectFactory;
import org.testng.annotations.Optional;
import org.testng.annotations.Parameters;
+import org.testng.annotations.ParameterCollector;
import org.testng.annotations.Test;
import org.testng.annotations.TestInstance;
import org.testng.internal.ConstructorOrMethod;
@@ -260,6 +261,20 @@ public boolean hasTestInstance(Method method, int i) {
return false;
}
+ @Override
+ public boolean hasParameterCollector(Method method, int i) {
+ final Annotation[][] annotations = method.getParameterAnnotations();
+ if (annotations.length > 0 && annotations[i].length > 0) {
+ final Annotation[] pa = annotations[i];
+ for (Annotation a : pa) {
+ if (a instanceof ParameterCollector) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
@Override
public String[] findOptionalValues(Method method) {
return optionalValues(method.getParameterAnnotations());
diff --git a/src/test/java/test/dataprovider/typed/TypedDataProviderSample.java b/src/test/java/test/dataprovider/typed/TypedDataProviderSample.java
new file mode 100644
index 0000000000..c75a96f7de
--- /dev/null
+++ b/src/test/java/test/dataprovider/typed/TypedDataProviderSample.java
@@ -0,0 +1,30 @@
+package test.dataprovider.typed;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.ParameterCollector;
+import org.testng.annotations.Test;
+
+public class TypedDataProviderSample {
+
+ private boolean dpDone = false;
+
+ @Test(dataProvider = "dp")
+ public void doStuff(String a, int b) {
+ if (!dpDone) throw new RuntimeException("Method should not be actually called by data provider");
+ }
+
+ @DataProvider(name = "dp")
+ public void createData(@ParameterCollector TypedDataProviderSample test) {
+ test.doStuff("test1", 1);
+ test.doStuff("test2", 2);
+ dpDone = true;
+ }
+
+ @Test(dataProvider = "dp2")
+ public void doStuff2(String a, int b) {}
+
+ @DataProvider(name = "dp2")
+ public void createData2(@ParameterCollector TypedDataProviderSample test) {
+ test.doStuff("test1", 1); // calling wrong method, should be doStuff2
+ }
+}
diff --git a/src/test/java/test/dataprovider/typed/TypedDataProviderTest.java b/src/test/java/test/dataprovider/typed/TypedDataProviderTest.java
new file mode 100644
index 0000000000..d769273c59
--- /dev/null
+++ b/src/test/java/test/dataprovider/typed/TypedDataProviderTest.java
@@ -0,0 +1,25 @@
+package test.dataprovider.typed;
+
+import org.testng.annotations.Test;
+import test.InvokedMethodNameListener;
+import test.SimpleBaseTest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TypedDataProviderTest extends SimpleBaseTest {
+
+ @Test
+ public void typedDataProviderWorks() {
+ InvokedMethodNameListener listener = run(true, TypedDataProviderSample.class);
+ assertThat(listener.getSucceedMethodNames())
+ .containsExactly(
+ "doStuff(test1,1)",
+ "doStuff(test2,2)"
+ );
+ final Throwable throwable = listener.getResult("doStuff2").getThrowable();
+ assertThat(throwable).isNotNull();
+ assertThat(throwable.getMessage())
+ .contains("Wrong method doStuff is called, expected doStuff2");
+ }
+
+}
diff --git a/src/test/resources/testng.xml b/src/test/resources/testng.xml
index f592e7ced7..a2a0f991b9 100644
--- a/src/test/resources/testng.xml
+++ b/src/test/resources/testng.xml
@@ -576,6 +576,7 @@
+