From 8bdc42be470cb2c08802bc3ee479f5075f966d87 Mon Sep 17 00:00:00 2001 From: Dzmitry Sankouski Date: Mon, 21 Feb 2022 10:32:15 +0300 Subject: [PATCH] DataProvider: possibility to unload dataprovider class, when done with it With current Data providers implementation, it's code will stick around in method area (JVM spec $2.5.4) for the entire test run. By specifying dataprovider class with it's full qualified name, and by using new custom classloader to load it, when needed, JVM gets a chance to unload dataprovider class, when we're done with it. Testing dataprovider class unload is performed by analysing memory dumps. Also, there is a test(comparePerformanceAgainstCsvFiles) to measure performance of data as code approach against common data approch, where data is stored in csv files. --- CHANGES.txt | 1 + .../testng/annotations/ITestAnnotation.java | 4 + .../java/org/testng/annotations/Test.java | 2 + .../internal/annotations/IDataProvidable.java | 4 + .../testng/internal/DataProviderLoader.java | 43 ++++++ .../testng/internal/DataProviderMethod.java | 4 +- .../internal/DataProviderMethodRemovable.java | 20 +++ .../java/org/testng/internal/Parameters.java | 34 ++++- .../annotations/FactoryAnnotation.java | 11 ++ .../annotations/JDK15AnnotationFinder.java | 4 +- .../internal/annotations/JDK15TagFactory.java | 1 + .../internal/annotations/TestAnnotation.java | 11 ++ .../DynamicDataProviderLoadingTest.kt | 126 ++++++++++++++++++ .../sample/issue2724/DataProviders.kt | 14 ++ .../sample/issue2724/SampleDPUnloaded.kt | 23 ++++ .../sample/issue2724/SampleDynamicDP.kt | 15 +++ .../sample/issue2724/SampleSimpleDP.kt | 21 +++ .../sample/issue2724/SampleWithCSVData.kt | 21 +++ .../sample/issue2724/TestTimeListener.kt | 20 +++ .../src/test/kotlin/test/SimpleBaseTest.kt | 19 ++- .../test/resources/test/issue2724/data.csv | 3 + testng-core/src/test/resources/testng.xml | 1 + testng-core/testng-core-build.gradle.kts | 3 + versions.properties | 6 + 24 files changed, 405 insertions(+), 6 deletions(-) create mode 100644 testng-core/src/main/java/org/testng/internal/DataProviderLoader.java create mode 100644 testng-core/src/main/java/org/testng/internal/DataProviderMethodRemovable.java create mode 100644 testng-core/src/test/kotlin/org/testng/dataprovider/DynamicDataProviderLoadingTest.kt create mode 100644 testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/DataProviders.kt create mode 100644 testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/SampleDPUnloaded.kt create mode 100644 testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/SampleDynamicDP.kt create mode 100644 testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/SampleSimpleDP.kt create mode 100644 testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/SampleWithCSVData.kt create mode 100644 testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/TestTimeListener.kt create mode 100644 testng-core/src/test/resources/test/issue2724/data.csv diff --git a/CHANGES.txt b/CHANGES.txt index 03d85128e9..5efdbea888 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,6 @@ Current 7.6.0 +New: GITHUB-2724: DataProvider: possibility to unload dataprovider class, when done with it (Dzmitry Sankouski) Fixed: GITHUB-217: Configure TestNG to fail when there's a failure in data provider (Krishnan Mahadevan) Fixed: GITHUB-2743: SuiteRunner could not be initial by default Configuration (Nan Liang) Fixed: GITHUB-2729: beforeConfiguration() listener method should be invoked for skipped configurations as well(Nan Liang) diff --git a/testng-core-api/src/main/java/org/testng/annotations/ITestAnnotation.java b/testng-core-api/src/main/java/org/testng/annotations/ITestAnnotation.java index 4ab3ed8b94..262a819998 100644 --- a/testng-core-api/src/main/java/org/testng/annotations/ITestAnnotation.java +++ b/testng-core-api/src/main/java/org/testng/annotations/ITestAnnotation.java @@ -72,6 +72,10 @@ public interface ITestAnnotation extends ITestOrConfiguration, IDataProvidable { void setDataProviderClass(Class v); + String getDataProviderDynamicClass(); + + void setDataProviderDynamicClass(String v); + void setRetryAnalyzer(Class c); Class getRetryAnalyzerClass(); diff --git a/testng-core-api/src/main/java/org/testng/annotations/Test.java b/testng-core-api/src/main/java/org/testng/annotations/Test.java index eda8fbad7c..23f72f6c26 100644 --- a/testng-core-api/src/main/java/org/testng/annotations/Test.java +++ b/testng-core-api/src/main/java/org/testng/annotations/Test.java @@ -104,6 +104,8 @@ */ Class dataProviderClass() default Object.class; + String dataProviderDynamicClass() default ""; + /** * If set to true, this test method will always be run even if it depends on a method that failed. * This attribute will be ignored if this test doesn't depend on any method or group. diff --git a/testng-core-api/src/main/java/org/testng/internal/annotations/IDataProvidable.java b/testng-core-api/src/main/java/org/testng/internal/annotations/IDataProvidable.java index 1b8896a9ec..075fd9cf3b 100644 --- a/testng-core-api/src/main/java/org/testng/internal/annotations/IDataProvidable.java +++ b/testng-core-api/src/main/java/org/testng/internal/annotations/IDataProvidable.java @@ -9,4 +9,8 @@ public interface IDataProvidable { Class getDataProviderClass(); void setDataProviderClass(Class v); + + String getDataProviderDynamicClass(); + + void setDataProviderDynamicClass(String v); } diff --git a/testng-core/src/main/java/org/testng/internal/DataProviderLoader.java b/testng-core/src/main/java/org/testng/internal/DataProviderLoader.java new file mode 100644 index 0000000000..8f27cf3245 --- /dev/null +++ b/testng-core/src/main/java/org/testng/internal/DataProviderLoader.java @@ -0,0 +1,43 @@ +package org.testng.internal; + +import java.io.IOException; +import java.io.InputStream; +import org.testng.log4testng.Logger; + +public class DataProviderLoader extends ClassLoader { + private static final int BUFFER_SIZE = 1 << 20; + private static final Logger log = Logger.getLogger(DataProviderLoader.class); + + public Class loadClazz(String path) throws ClassNotFoundException { + Class clazz = findLoadedClass(path); + if (clazz == null) { + byte[] bt = loadClassData(path); + clazz = defineClass(path, bt, 0, bt.length); + } + + return clazz; + } + + private byte[] loadClassData(String className) throws ClassNotFoundException { + InputStream in = + this.getClass() + .getClassLoader() + .getResourceAsStream(className.replace(".", "/") + ".class"); + if (in == null) { + throw new ClassNotFoundException("Cannot load resource input stream: " + className); + } + + byte[] classBytes; + try { + classBytes = in.readAllBytes(); + } catch (IOException e) { + throw new ClassNotFoundException("ERROR reading class file" + e); + } + + if (classBytes == null) { + throw new ClassNotFoundException("Cannot load class: " + className); + } + + return classBytes; + } +} diff --git a/testng-core/src/main/java/org/testng/internal/DataProviderMethod.java b/testng-core/src/main/java/org/testng/internal/DataProviderMethod.java index 4ef2336a86..dfbc3a5259 100644 --- a/testng-core/src/main/java/org/testng/internal/DataProviderMethod.java +++ b/testng-core/src/main/java/org/testng/internal/DataProviderMethod.java @@ -8,8 +8,8 @@ /** Represents an @{@link org.testng.annotations.DataProvider} annotated method. */ class DataProviderMethod implements IDataProviderMethod { - private final Object instance; - private final Method method; + protected Object instance; + protected Method method; private final IDataProviderAnnotation annotation; DataProviderMethod(Object instance, Method method, IDataProviderAnnotation annotation) { diff --git a/testng-core/src/main/java/org/testng/internal/DataProviderMethodRemovable.java b/testng-core/src/main/java/org/testng/internal/DataProviderMethodRemovable.java new file mode 100644 index 0000000000..b4c1a98171 --- /dev/null +++ b/testng-core/src/main/java/org/testng/internal/DataProviderMethodRemovable.java @@ -0,0 +1,20 @@ +package org.testng.internal; + +import java.lang.reflect.Method; +import org.testng.annotations.IDataProviderAnnotation; + +/** Represents an @{@link org.testng.annotations.DataProvider} annotated method. */ +class DataProviderMethodRemovable extends DataProviderMethod { + + DataProviderMethodRemovable(Object instance, Method method, IDataProviderAnnotation annotation) { + super(instance, method, annotation); + } + + public void setInstance(Object instance) { + this.instance = instance; + } + + public void setMethod(Method method) { + this.method = method; + } +} diff --git a/testng-core/src/main/java/org/testng/internal/Parameters.java b/testng-core/src/main/java/org/testng/internal/Parameters.java index 2ce13c961b..079f45b734 100644 --- a/testng-core/src/main/java/org/testng/internal/Parameters.java +++ b/testng-core/src/main/java/org/testng/internal/Parameters.java @@ -495,6 +495,15 @@ private static IDataProviderMethod findDataProvider( if (dp != null) { String dataProviderName = dp.getDataProvider(); Class dataProviderClass = dp.getDataProviderClass(); + boolean isDynamicDataProvider = + dataProviderClass == null && !dp.getDataProviderDynamicClass().isEmpty(); + if (isDynamicDataProvider) { + try { + dataProviderClass = new DataProviderLoader().loadClazz(dp.getDataProviderDynamicClass()); + } catch (ClassNotFoundException e) { + throw new TestNGException("Dynamic data provider class %s not found", e); + } + } if (!Utils.isStringEmpty(dataProviderName)) { result = @@ -505,6 +514,7 @@ private static IDataProviderMethod findDataProvider( finder, dataProviderName, dataProviderClass, + isDynamicDataProvider, context); if (null == result) { @@ -566,6 +576,10 @@ private static IDataProvidable merge(ITestAnnotation methodLevel, ITestAnnotatio if (isDataProviderClassEmpty(methodLevel) && !isDataProviderClassEmpty(classLevel)) { methodLevel.setDataProviderClass(classLevel.getDataProviderClass()); } + if (isDynamicDataProviderClassEmpty(methodLevel) + && !isDynamicDataProviderClassEmpty(classLevel)) { + methodLevel.setDataProviderDynamicClass(classLevel.getDataProviderDynamicClass()); + } return methodLevel; } @@ -574,6 +588,10 @@ private static boolean isDataProviderClassEmpty(ITestAnnotation annotation) { || Object.class.equals(annotation.getDataProviderClass()); } + private static boolean isDynamicDataProviderClassEmpty(ITestAnnotation annotation) { + return annotation.getDataProviderDynamicClass().isEmpty(); + } + private static boolean isDataProviderNameEmpty(ITestAnnotation annotation) { return Strings.isNullOrEmpty(annotation.getDataProvider()); } @@ -586,6 +604,7 @@ private static IDataProviderMethod findDataProvider( IAnnotationFinder finder, String name, Class dataProviderClass, + boolean isDynamicDataProvider, ITestContext context) { IDataProviderMethod result = null; @@ -620,7 +639,12 @@ private static IDataProviderMethod findDataProvider( if (result != null) { throw new TestNGException("Found two providers called '" + name + "' on " + cls); } - result = new DataProviderMethod(instanceToUse, m, dp); + + if (isDynamicDataProvider) { + result = new DataProviderMethodRemovable(instanceToUse, m, dp); + } else { + result = new DataProviderMethod(instanceToUse, m, dp); + } } } @@ -839,6 +863,14 @@ public void remove() { filteredParameters, dataProviderMethod, testMethod, methodParams.context); } + if (dataProviderMethod instanceof DataProviderMethodRemovable) { + ((DataProviderMethodRemovable) dataProviderMethod).setMethod(null); + ((DataProviderMethodRemovable) dataProviderMethod).setInstance(null); + if (testMethod instanceof TestNGMethod) { + ((TestNGMethod) testMethod).setDataProviderMethod(null); + } + } + return new ParameterHolder( filteredParameters, ParameterOrigin.ORIGIN_DATA_PROVIDER, dataProviderMethod); } else if (methodParams.xmlParameters.isEmpty()) { diff --git a/testng-core/src/main/java/org/testng/internal/annotations/FactoryAnnotation.java b/testng-core/src/main/java/org/testng/internal/annotations/FactoryAnnotation.java index 88702324b9..1b61450fdb 100644 --- a/testng-core/src/main/java/org/testng/internal/annotations/FactoryAnnotation.java +++ b/testng-core/src/main/java/org/testng/internal/annotations/FactoryAnnotation.java @@ -8,6 +8,7 @@ public class FactoryAnnotation extends BaseAnnotation implements IFactoryAnnotat private String m_dataProvider = null; private Class m_dataProviderClass; + private String m_dataProviderDynamicClass; private boolean m_enabled = true; private List m_indices; @@ -30,6 +31,16 @@ public Class getDataProviderClass() { return m_dataProviderClass; } + @Override + public String getDataProviderDynamicClass() { + return m_dataProviderDynamicClass; + } + + @Override + public void setDataProviderDynamicClass(String v) { + m_dataProviderDynamicClass = v; + } + @Override public boolean getEnabled() { return m_enabled; diff --git a/testng-core/src/main/java/org/testng/internal/annotations/JDK15AnnotationFinder.java b/testng-core/src/main/java/org/testng/internal/annotations/JDK15AnnotationFinder.java index cbe2a1299f..5121faae44 100644 --- a/testng-core/src/main/java/org/testng/internal/annotations/JDK15AnnotationFinder.java +++ b/testng-core/src/main/java/org/testng/internal/annotations/JDK15AnnotationFinder.java @@ -48,7 +48,7 @@ public class JDK15AnnotationFinder implements IAnnotationFinder { private final JDK15TagFactory m_tagFactory = new JDK15TagFactory(); private final Map, Class> m_annotationMap = new ConcurrentHashMap<>(); - private final Map, IAnnotation> m_annotations = new ConcurrentHashMap<>(); + private final Map m_annotations = new ConcurrentHashMap<>(); private final IAnnotationTransformer m_transformer; @@ -273,7 +273,7 @@ private A findAnnotation( IAnnotation result = m_annotations.computeIfAbsent( - p, + p.toString(), key -> { IAnnotation obj = m_tagFactory.createTag(cls, testMethod, a, annotationClass); transform(obj, testClass, testConstructor, testMethod, whichClass); diff --git a/testng-core/src/main/java/org/testng/internal/annotations/JDK15TagFactory.java b/testng-core/src/main/java/org/testng/internal/annotations/JDK15TagFactory.java index 68feb7ddf0..7fc2bdf81d 100644 --- a/testng-core/src/main/java/org/testng/internal/annotations/JDK15TagFactory.java +++ b/testng-core/src/main/java/org/testng/internal/annotations/JDK15TagFactory.java @@ -484,6 +484,7 @@ private IAnnotation createTestTag(Class cls, Annotation a) { result.setDataProviderClass( findInherited( test.dataProviderClass(), cls, Test.class, "dataProviderClass", DEFAULT_CLASS)); + result.setDataProviderDynamicClass(test.dataProviderDynamicClass()); result.setAlwaysRun(test.alwaysRun()); result.setDescription( findInherited(test.description(), cls, Test.class, "description", DEFAULT_STRING)); diff --git a/testng-core/src/main/java/org/testng/internal/annotations/TestAnnotation.java b/testng-core/src/main/java/org/testng/internal/annotations/TestAnnotation.java index 450ed8cefc..cbdf3d26d1 100644 --- a/testng-core/src/main/java/org/testng/internal/annotations/TestAnnotation.java +++ b/testng-core/src/main/java/org/testng/internal/annotations/TestAnnotation.java @@ -19,6 +19,7 @@ public class TestAnnotation extends TestOrConfiguration implements ITestAnnotati private String m_testName = ""; private boolean m_singleThreaded = false; private Class m_dataProviderClass = null; + private String m_dataProviderDynamicClass = null; private Class m_retryAnalyzerClass = null; private boolean m_skipFailedInvocations = false; private boolean m_ignoreMissingDependencies = false; @@ -66,6 +67,16 @@ public void setDataProviderClass(Class dataProviderClass) { m_dataProviderClass = dataProviderClass; } + @Override + public String getDataProviderDynamicClass() { + return m_dataProviderDynamicClass; + } + + @Override + public void setDataProviderDynamicClass(String v) { + m_dataProviderDynamicClass = v; + } + @Override public void setInvocationCount(int invocationCount) { m_invocationCount = invocationCount; diff --git a/testng-core/src/test/kotlin/org/testng/dataprovider/DynamicDataProviderLoadingTest.kt b/testng-core/src/test/kotlin/org/testng/dataprovider/DynamicDataProviderLoadingTest.kt new file mode 100644 index 0000000000..754934d732 --- /dev/null +++ b/testng-core/src/test/kotlin/org/testng/dataprovider/DynamicDataProviderLoadingTest.kt @@ -0,0 +1,126 @@ +package org.testng.dataprovider + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.SoftAssertions +import org.netbeans.lib.profiler.heap.HeapFactory2 +import org.netbeans.lib.profiler.heap.Instance +import org.netbeans.lib.profiler.heap.JavaClass +import org.testng.Reporter +import org.testng.annotations.Test +import org.testng.dataprovider.sample.issue2724.* +import test.SimpleBaseTest +import java.io.File +import java.nio.file.Files + +const val CLASS_NAME_DP = "org.testng.dataprovider.sample.issue2724.DataProviders" +const val CLASS_NAME_DP_LOADER = "org.testng.internal.DataProviderLoader" + +class DynamicDataProviderLoadingTest : SimpleBaseTest() { + + @Test + fun testDynamicDataProviderPasses() { + val listener = run(SampleDynamicDP::class.java) + assertThat(listener.failedMethodNames).isEmpty() + assertThat(listener.succeedMethodNames).containsExactly( + "testDynamicDataProvider(Mike,34,student)", + "testDynamicDataProvider(Mike,23,driver)", + "testDynamicDataProvider(Paul,20,director)", + ) + assertThat(listener.skippedMethodNames).isEmpty() + } + + @Test + fun testDynamicDataProviderUnloaded() { + val tempDirectory = Files.createTempDirectory("temp-testng-") + val dumpPath = "%s/%s".format(tempDirectory.toAbsolutePath().toString(), "dump.hprof") + val dumpPathBeforeSample = + "%s/%s".format(tempDirectory.toAbsolutePath().toString(), "dump-before-sample.hprof") + System.setProperty("memdump.path", dumpPath) + + saveMemDump(dumpPathBeforeSample) + val heapDumpBeforeSampleFile = File(dumpPathBeforeSample) + assertThat(heapDumpBeforeSampleFile).exists() + var heap = HeapFactory2.createHeap(heapDumpBeforeSampleFile, null) + val beforeSampleDPClassDump: JavaClass? = heap.getJavaClassByName(CLASS_NAME_DP) + assertThat(beforeSampleDPClassDump) + .describedAs( + "Class $CLASS_NAME_DP shouldn't be loaded, before test sample started. " + ) + .isNull() + + run(SampleDPUnloaded::class.java) + + val heapDumpFile = File(dumpPath) + assertThat(heapDumpFile).exists() + heap = HeapFactory2.createHeap(heapDumpFile, null) + + with(SoftAssertions()) { + val dpLoaderClassDump: JavaClass? = heap.getJavaClassByName(CLASS_NAME_DP_LOADER) + val dpClassDump: JavaClass? = heap.getJavaClassByName(CLASS_NAME_DP) + val dpLoaderMessage = dpLoaderClassDump?.instances?.joinToString("\n") { + getGCPath(it) + } + val dpMessage = dpLoaderClassDump?.instances?.joinToString("\n") { + getGCPath(it) + } + + this.assertThat(dpLoaderClassDump?.instances) + .describedAs( + """ + All instances of class $CLASS_NAME_DP_LOADER should be garbage collected, but was not. + Path to GC root is: + $dpLoaderMessage + """.trimIndent() + ) + .isEmpty() + this.assertThat(dpClassDump) + .describedAs( + """ + Class $CLASS_NAME_DP shouldn't be loaded, but it was. + Path to GC root is: + $dpMessage + """.trimIndent() + ) + .isNull() + this.assertAll() + } + } + + @Test + fun comparePerformanceAgainstCsvFiles() { + val simpleDPSuite = create().apply { + setTestClasses(arrayOf(SampleSimpleDP::class.java)) + setListenerClasses(listOf(TestTimeListener::class.java)) + } + val csvSuite = create().apply { + setTestClasses(arrayOf(SampleWithCSVData::class.java)) + setListenerClasses(listOf(TestTimeListener::class.java)) + } + val dataAsCodeSuite = create().apply { + setTestClasses(arrayOf(SampleDynamicDP::class.java)) + setListenerClasses(listOf(TestTimeListener::class.java)) + } + + Reporter.log("Test execution time:\n") + for (suite in listOf( + Pair("simple dataprovider", simpleDPSuite), + Pair("dataprovider as code", dataAsCodeSuite), + Pair("csv dataprovider", csvSuite), + )) { + run(false, suite.second) + Reporter.log( + "${suite.first} execution times: %d milliseconds." + .format(TestTimeListener.testRunTime), + true + ) + } + } + + fun getGCPath(instance: Instance): String { + var result = "" + if (!instance.isGCRoot) { + result += getGCPath(instance.nearestGCRootPointer) + } + return result + "${instance.javaClass.name}\n" + } +} diff --git a/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/DataProviders.kt b/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/DataProviders.kt new file mode 100644 index 0000000000..99968d6b61 --- /dev/null +++ b/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/DataProviders.kt @@ -0,0 +1,14 @@ +package org.testng.dataprovider.sample.issue2724 + +import org.testng.annotations.DataProvider + +class DataProviders { + @DataProvider + fun data() : Array> { + return arrayOf( + arrayOf("Mike", 34, "student"), + arrayOf("Mike", 23, "driver"), + arrayOf("Paul", 20, "director") + ) + } +} diff --git a/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/SampleDPUnloaded.kt b/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/SampleDPUnloaded.kt new file mode 100644 index 0000000000..6ea906e326 --- /dev/null +++ b/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/SampleDPUnloaded.kt @@ -0,0 +1,23 @@ +package org.testng.dataprovider.sample.issue2724 + +import jlibs.core.lang.RuntimeUtil +import org.testng.annotations.AfterClass +import org.testng.annotations.Test +import test.SimpleBaseTest + +class SampleDPUnloaded { + @Suppress("UNUSED_PARAMETER") + @Test( + dataProviderDynamicClass = "org.testng.dataprovider.sample.issue2724.DataProviders", + dataProvider = "data" + ) + fun testDynamicDataProvider(name: String, age: Int, status: String) { + + } + + @AfterClass + fun afterTest() { + RuntimeUtil.gc(10) + SimpleBaseTest.saveMemDump(System.getProperty("memdump.path")) + } +} diff --git a/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/SampleDynamicDP.kt b/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/SampleDynamicDP.kt new file mode 100644 index 0000000000..ccf34d65f3 --- /dev/null +++ b/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/SampleDynamicDP.kt @@ -0,0 +1,15 @@ +package org.testng.dataprovider.sample.issue2724 + +import org.testng.annotations.Test + +class SampleDynamicDP { + + @Suppress("UNUSED_PARAMETER") + @Test( + dataProviderDynamicClass = "org.testng.dataprovider.sample.issue2724.DataProviders", + dataProvider = "data" + ) + fun testDynamicDataProvider(name: String, age: Int, status: String) { + + } +} diff --git a/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/SampleSimpleDP.kt b/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/SampleSimpleDP.kt new file mode 100644 index 0000000000..2cf2274d29 --- /dev/null +++ b/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/SampleSimpleDP.kt @@ -0,0 +1,21 @@ +package org.testng.dataprovider.sample.issue2724 + +import org.testng.annotations.DataProvider +import org.testng.annotations.Test + +class SampleSimpleDP { + @Suppress("UNUSED_PARAMETER") + @Test(dataProvider = "data") + fun testDynamicDataProvider(name: String, age: Int, status: String) { + + } + + @DataProvider + fun data() : Array> { + return arrayOf( + arrayOf("Mike", 34, "student"), + arrayOf("Mike", 23, "driver"), + arrayOf("Paul", 20, "director") + ) + } +} diff --git a/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/SampleWithCSVData.kt b/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/SampleWithCSVData.kt new file mode 100644 index 0000000000..598a9fe913 --- /dev/null +++ b/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/SampleWithCSVData.kt @@ -0,0 +1,21 @@ +package org.testng.dataprovider.sample.issue2724 + +import org.testng.annotations.DataProvider +import org.testng.annotations.Test + +class SampleWithCSVData { + @Suppress("UNUSED_PARAMETER") + @Test(dataProvider = "data") + fun testDynamicDataProvider(name: String, age: Int, status: String) { + + } + + @DataProvider + fun data() : Array> { + val fileInputStream = this::class.java.classLoader + .getResourceAsStream("test/issue2724/data.csv") + return fileInputStream.reader().readLines().map { + it.split(",").toTypedArray() + }.toTypedArray() + } +} diff --git a/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/TestTimeListener.kt b/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/TestTimeListener.kt new file mode 100644 index 0000000000..f19db126c1 --- /dev/null +++ b/testng-core/src/test/kotlin/org/testng/dataprovider/sample/issue2724/TestTimeListener.kt @@ -0,0 +1,20 @@ +package org.testng.dataprovider.sample.issue2724 + +import org.testng.ITestContext +import org.testng.ITestListener + +class TestTimeListener : ITestListener { + private var startTime: Long = 0 + + override fun onStart(context: ITestContext?) { + startTime = System.currentTimeMillis() + } + + override fun onFinish(context: ITestContext?) { + testRunTime = System.currentTimeMillis() - startTime + } + + companion object { + var testRunTime: Long = 0 + } +} diff --git a/testng-core/src/test/kotlin/test/SimpleBaseTest.kt b/testng-core/src/test/kotlin/test/SimpleBaseTest.kt index 183d0b2df3..90eb62d003 100644 --- a/testng-core/src/test/kotlin/test/SimpleBaseTest.kt +++ b/testng-core/src/test/kotlin/test/SimpleBaseTest.kt @@ -1,5 +1,6 @@ package test +import com.sun.management.HotSpotDiagnosticMXBean import org.assertj.core.api.Assertions.assertThat import org.testng.* import org.testng.annotations.ITestAnnotation @@ -10,6 +11,7 @@ import org.testng.internal.annotations.JDK15AnnotationFinder import org.testng.xml.* import org.testng.xml.internal.Parser import java.io.* +import java.lang.management.ManagementFactory import java.nio.file.FileVisitResult import java.nio.file.Files import java.nio.file.Path @@ -28,7 +30,7 @@ open class SimpleBaseTest { fun run(vararg testClasses: Class<*>) = run(false, *testClasses) @JvmStatic - private fun run(skipConfiguration: Boolean, tng: TestNG) = + fun run(skipConfiguration: Boolean, tng: TestNG) = InvokedMethodNameListener(skipConfiguration).apply { tng.addListener(this) tng.run() @@ -382,6 +384,21 @@ open class SimpleBaseTest { .collect(Collectors.joining("\n")) return "Failed methods should pass: \n $methods" } + + @JvmStatic + fun saveMemDump(path: String) { + try { + val server = ManagementFactory.getPlatformMBeanServer() + val mxBean = ManagementFactory.newPlatformMXBeanProxy( + server, + "com.sun.management:type=HotSpotDiagnostic", + HotSpotDiagnosticMXBean::class.java + ) + mxBean.dumpHeap(path, true) + } catch (e: IOException) { + throw TestNGException("Failed to save memory dump", e) + } + } } class TestNGFileVisitor : SimpleFileVisitor() { diff --git a/testng-core/src/test/resources/test/issue2724/data.csv b/testng-core/src/test/resources/test/issue2724/data.csv new file mode 100644 index 0000000000..14675337c3 --- /dev/null +++ b/testng-core/src/test/resources/test/issue2724/data.csv @@ -0,0 +1,3 @@ +Mike, 34, student +Mike, 23, driver +Paul, 20, director diff --git a/testng-core/src/test/resources/testng.xml b/testng-core/src/test/resources/testng.xml index 5ac8b0caa1..5ba6547e2a 100644 --- a/testng-core/src/test/resources/testng.xml +++ b/testng-core/src/test/resources/testng.xml @@ -992,6 +992,7 @@ + diff --git a/testng-core/testng-core-build.gradle.kts b/testng-core/testng-core-build.gradle.kts index dfec4a600c..6d91da6fa4 100644 --- a/testng-core/testng-core-build.gradle.kts +++ b/testng-core/testng-core-build.gradle.kts @@ -46,6 +46,9 @@ dependencies { testImplementation("org.jboss.shrinkwrap:shrinkwrap-api:_") testImplementation("org.jboss.shrinkwrap:shrinkwrap-impl-base:_") testImplementation("org.xmlunit:xmlunit-assertj:_") + testImplementation("in.jlibs:jlibs-core:_") + testImplementation("org.gridkit.jvmtool:heaplib:_") + testImplementation("org.gridkit.lab:jvm-attach-api:_") testImplementation("commons-io:commons-io:_") } diff --git a/versions.properties b/versions.properties index 287f41cfcc..878df28468 100644 --- a/versions.properties +++ b/versions.properties @@ -25,6 +25,8 @@ version.com.google.inject..guice-bom=5.0.1 version.commons-io..commons-io=2.11.0 +version.in.jlibs..jlibs-core=3.0.1 + version.javax..javaee-api=8.0.1 version.junit.junit=4.13.2 @@ -43,6 +45,10 @@ version.org.assertj..assertj-core=3.22.0 version.org.codehaus.groovy..groovy-all=2.4.7 +version.org.gridkit.jvmtool..heaplib=0.2 + +version.org.gridkit.lab..jvm-attach-api=1.5 + version.org.jboss.shrinkwrap..shrinkwrap-api=1.2.6 version.org.jboss.shrinkwrap..shrinkwrap-impl-base=1.2.6