From 88cb15fe032ec86e8099d93129763a2899e8e53a Mon Sep 17 00:00:00 2001 From: Krishnan Mahadevan Date: Mon, 29 Apr 2024 19:17:41 +0530 Subject: [PATCH] Report deadlocks when running in sharedthreadpool Closes #3028 --- CHANGES.txt | 1 + .../internal/TestNGDeadLockException.java | 10 +++++ .../src/main/java/org/testng/TestRunner.java | 33 ++++++++++++++-- .../test/thread/SharedThreadPoolTest.java | 26 +++++++++++++ .../AnotherDataDrivenTestSample.java | 30 +++++++++++++++ .../issue3028/DataDrivenTestSample.java | 29 ++++++++++++++ .../FactoryPoweredDataDrivenTestSample.java | 38 +++++++++++++++++++ 7 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 testng-core-api/src/main/java/org/testng/internal/TestNGDeadLockException.java create mode 100644 testng-core/src/test/java/test/thread/issue3028/AnotherDataDrivenTestSample.java create mode 100644 testng-core/src/test/java/test/thread/issue3028/DataDrivenTestSample.java create mode 100644 testng-core/src/test/java/test/thread/issue3028/FactoryPoweredDataDrivenTestSample.java diff --git a/CHANGES.txt b/CHANGES.txt index 9fed42440..b7ee0fe5f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ Current (7.11.0) +Fixed: GITHUB-3028: Execution stalls when using "use-global-thread-pool" (Krishnan Mahadevan) 7.10.2 Fixed: GITHUB-3117: ListenerComparator doesn't work (Krishnan Mahadevan) diff --git a/testng-core-api/src/main/java/org/testng/internal/TestNGDeadLockException.java b/testng-core-api/src/main/java/org/testng/internal/TestNGDeadLockException.java new file mode 100644 index 000000000..a499bb5c1 --- /dev/null +++ b/testng-core-api/src/main/java/org/testng/internal/TestNGDeadLockException.java @@ -0,0 +1,10 @@ +package org.testng.internal; + +import org.testng.TestNGException; + +/** Represents a deadlock condition in TestNG wherein execution can get stalled. */ +public class TestNGDeadLockException extends TestNGException { + public TestNGDeadLockException(String string) { + super(string); + } +} diff --git a/testng-core/src/main/java/org/testng/TestRunner.java b/testng-core/src/main/java/org/testng/TestRunner.java index 8f8085c07..5d85b6047 100644 --- a/testng-core/src/main/java/org/testng/TestRunner.java +++ b/testng-core/src/main/java/org/testng/TestRunner.java @@ -47,6 +47,7 @@ import org.testng.internal.TestMethodComparator; import org.testng.internal.TestMethodContainer; import org.testng.internal.TestNGClassFinder; +import org.testng.internal.TestNGDeadLockException; import org.testng.internal.TestNGMethodFinder; import org.testng.internal.Utils; import org.testng.internal.XmlMethodSelector; @@ -63,6 +64,7 @@ import org.testng.util.TimeUtils; import org.testng.xml.XmlClass; import org.testng.xml.XmlPackage; +import org.testng.xml.XmlSuite; import org.testng.xml.XmlTest; /** This class takes care of running one Test. */ @@ -807,9 +809,34 @@ public List> createWorkers(List methods) { .testContext(this) .listeners(this.m_classListeners.values()) .build(); - return AbstractParallelWorker.newWorker( - m_xmlTest.getParallel(), m_xmlTest.getGroupByInstances()) - .createWorkers(args); + List> result = + AbstractParallelWorker.newWorker(m_xmlTest.getParallel(), m_xmlTest.getGroupByInstances()) + .createWorkers(args); + long dataDrivenTestCount = + result.stream() + .flatMap(it -> it.getTasks().stream()) + .filter(ITestNGMethod::isDataDriven) + .count(); + int threads = getCurrentXmlTest().getThreadCount(); + XmlSuite.ParallelMode parallelMode = getCurrentXmlTest().getParallel(); + XmlSuite suite = getSuite().getXmlSuite(); + if (suite.useGlobalThreadPool() + && parallelMode.isParallel() + && dataDrivenTestCount >= threads) { + String msg = + "[Deadlock condition detected] " + + "Cannot run " + + dataDrivenTestCount + + " data driven tests on just " + + threads + + " threads when " + + "using common thread pool. " + + "Please increase the number of threads to at-least " + + (dataDrivenTestCount + 1) + + "."; + throw new TestNGDeadLockException(msg); + } + return result; } private void afterRun() { diff --git a/testng-core/src/test/java/test/thread/SharedThreadPoolTest.java b/testng-core/src/test/java/test/thread/SharedThreadPoolTest.java index 8351bb227..a6b4c5b59 100644 --- a/testng-core/src/test/java/test/thread/SharedThreadPoolTest.java +++ b/testng-core/src/test/java/test/thread/SharedThreadPoolTest.java @@ -8,10 +8,15 @@ import java.util.stream.Collectors; import org.testng.TestNG; import org.testng.annotations.AfterMethod; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import org.testng.internal.TestNGDeadLockException; import org.testng.xml.XmlSuite; import test.SimpleBaseTest; import test.thread.issue2019.TestClassSample; +import test.thread.issue3028.AnotherDataDrivenTestSample; +import test.thread.issue3028.DataDrivenTestSample; +import test.thread.issue3028.FactoryPoweredDataDrivenTestSample; public class SharedThreadPoolTest extends SimpleBaseTest { @@ -68,6 +73,27 @@ public void ensureCommonThreadPoolIsNotUsedWhenUsedWithSuiteFiles() { .hasSizeBetween(4, 10); } + @Test(expectedExceptions = TestNGDeadLockException.class, dataProvider = "modes") + public void ensureDeadLocksAreDetectedForDataDrivenTestsRunningInParallel( + XmlSuite.ParallelMode mode, Class... classes) { + TestNG testng = create(classes); + testng.shouldUseGlobalThreadPool(true); + testng.setParallel(mode); + testng.setThreadCount(2); + testng.run(); + } + + @DataProvider(name = "modes") + public Object[][] parallelModes() { + return new Object[][] { + {XmlSuite.ParallelMode.METHODS, DataDrivenTestSample.class}, + {XmlSuite.ParallelMode.INSTANCES, FactoryPoweredDataDrivenTestSample.class}, + { + XmlSuite.ParallelMode.CLASSES, DataDrivenTestSample.class, AnotherDataDrivenTestSample.class + }, + }; + } + private static List runSimpleTest(String suite) { TestNG testng = create(); testng.setTestSuites(Collections.singletonList(suite)); diff --git a/testng-core/src/test/java/test/thread/issue3028/AnotherDataDrivenTestSample.java b/testng-core/src/test/java/test/thread/issue3028/AnotherDataDrivenTestSample.java new file mode 100644 index 000000000..ae7239b34 --- /dev/null +++ b/testng-core/src/test/java/test/thread/issue3028/AnotherDataDrivenTestSample.java @@ -0,0 +1,30 @@ +package test.thread.issue3028; + +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class AnotherDataDrivenTestSample { + + @Test(dataProvider = "dp") + public void a(int ignored) { + log(); + } + + @Test(dataProvider = "dp") + public void b(int ignored) { + log(); + } + + private void log() { + ITestResult itr = Reporter.getCurrentTestResult(); + long id = Thread.currentThread().getId(); + Reporter.log("Running " + itr.toString() + " on Thread " + id); + } + + @DataProvider(name = "dp", parallel = true) + public Object[][] getData() { + return new Object[][] {{1}, {2}, {3}, {4}, {5}}; + } +} diff --git a/testng-core/src/test/java/test/thread/issue3028/DataDrivenTestSample.java b/testng-core/src/test/java/test/thread/issue3028/DataDrivenTestSample.java new file mode 100644 index 000000000..51d1eadae --- /dev/null +++ b/testng-core/src/test/java/test/thread/issue3028/DataDrivenTestSample.java @@ -0,0 +1,29 @@ +package test.thread.issue3028; + +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.annotations.*; + +public class DataDrivenTestSample { + + @Test(dataProvider = "dp") + public void a(int ignored) { + log(); + } + + @Test(dataProvider = "dp") + public void b(int ignored) { + log(); + } + + private void log() { + ITestResult itr = Reporter.getCurrentTestResult(); + long id = Thread.currentThread().getId(); + Reporter.log("Running " + itr.toString() + " on Thread " + id); + } + + @DataProvider(name = "dp", parallel = true) + public Object[][] getData() { + return new Object[][] {{1}, {2}, {3}, {4}, {5}}; + } +} diff --git a/testng-core/src/test/java/test/thread/issue3028/FactoryPoweredDataDrivenTestSample.java b/testng-core/src/test/java/test/thread/issue3028/FactoryPoweredDataDrivenTestSample.java new file mode 100644 index 000000000..11af3fc0a --- /dev/null +++ b/testng-core/src/test/java/test/thread/issue3028/FactoryPoweredDataDrivenTestSample.java @@ -0,0 +1,38 @@ +package test.thread.issue3028; + +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Factory; +import org.testng.annotations.Test; + +public class FactoryPoweredDataDrivenTestSample { + + @Factory + public static Object[] objects() { + return new Object[] { + new FactoryPoweredDataDrivenTestSample(), new FactoryPoweredDataDrivenTestSample() + }; + } + + @Test(dataProvider = "dp") + public void a(int ignored) { + log(); + } + + @Test(dataProvider = "dp") + public void b(int ignored) { + log(); + } + + private void log() { + ITestResult itr = Reporter.getCurrentTestResult(); + long id = Thread.currentThread().getId(); + Reporter.log("Running " + itr.toString() + " on Thread " + id); + } + + @DataProvider(name = "dp", parallel = true) + public Object[][] getData() { + return new Object[][] {{1}, {2}, {3}, {4}, {5}}; + } +}