Skip to content

Commit 2466655

Browse files
committed
Streamline listener invocation handling exceptions
Closes #3238
1 parent 4c92177 commit 2466655

File tree

8 files changed

+100
-2
lines changed

8 files changed

+100
-2
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
Current (7.13.0)
2+
Fixed: GITHUB-3238: Tests never finish if helper throws exception while executing parallel tests ( Krishnan Mahadevan )
23
Fixed: GITHUB-3120: ITestNGListenerFactory is broken and never invoked (Krishnan Mahadevan)
34

45
7.12.0

testng-core/src/main/java/org/testng/internal/invokers/TestInvoker.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ class TestInvoker extends BaseInvoker implements ITestInvoker {
6262
private final List<IClassListener> m_classListeners;
6363
private final boolean m_skipFailedInvocationCounts;
6464

65+
// ThreadLocal to track if we're currently inside runTestResultListener()
66+
// Used to prevent duplicate listener invocations when a listener throws an exception.
67+
// See GITHUB-3238.
68+
private static final ThreadLocal<Boolean> isInvokingListeners =
69+
ThreadLocal.withInitial(() -> false);
70+
6571
public TestInvoker(
6672
ITestResultNotifier m_notifier,
6773
ITestContext m_testContext,
@@ -293,9 +299,13 @@ public void runTestResultListener(ITestResult tr) {
293299
m_notifier.getTestListeners(), m_configuration.getListenerComparator())
294300
: ListenerOrderDeterminer.order(
295301
m_notifier.getTestListeners(), m_configuration.getListenerComparator());
302+
isInvokingListeners.set(true);
296303
TestListenerHelper.runTestListeners(tr, listeners);
297304
TestListenerHelper.runTestListeners(
298305
tr, Collections.singletonList(m_notifier.getExitCodeListener()));
306+
// Only reset the flag on successful completion. If an exception is thrown,
307+
// we want the flag to remain true so the catch block can detect it.
308+
isInvokingListeners.set(false);
299309
}
300310

301311
private Collection<IDataProviderListener> dataProviderListeners() {
@@ -1020,7 +1030,16 @@ public int invoke(int invCount) {
10201030
.ifPresent(it -> TestResult.copyAttributes(it, r));
10211031
r.setStatus(TestResult.FAILURE);
10221032
result.add(r);
1023-
runTestResultListener(r);
1033+
// Only invoke listeners if the exception didn't come from a listener invocation.
1034+
// If we're inside a listener invocation, the exception came from a listener,
1035+
// so don't invoke listeners again to prevent duplicate invocations.
1036+
// See GITHUB-3238.
1037+
if (!isInvokingListeners.get()) {
1038+
runTestResultListener(r);
1039+
} else {
1040+
// Reset the flag since we're handling the listener exception here
1041+
isInvokingListeners.set(false);
1042+
}
10241043
m_notifier.addFailedTest(arguments.getTestMethod(), r);
10251044
} // catch
10261045
return invocationCount.get();

testng-core/src/main/java/org/testng/internal/thread/graph/TestNGFutureTask.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ protected void done() {
3131
} catch (InterruptedException | ExecutionException e) {
3232
throwable = e;
3333
}
34-
callback.accept(result, throwable);
34+
callback.accept(result == null ? worker : result, throwable);
3535
}
3636

3737
@Override

testng-core/src/test/java/test/listeners/ListenersTest.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@
1010
import java.util.HashMap;
1111
import java.util.List;
1212
import java.util.Map;
13+
import java.util.concurrent.atomic.AtomicInteger;
1314
import org.assertj.core.api.Assertions;
1415
import org.assertj.core.api.SoftAssertions;
1516
import org.testng.CommandLineArgs;
17+
import org.testng.ITestListener;
1618
import org.testng.ITestNGListener;
19+
import org.testng.ITestResult;
1720
import org.testng.TestNG;
1821
import org.testng.annotations.DataProvider;
1922
import org.testng.annotations.Test;
2023
import org.testng.internal.ExitCode;
2124
import org.testng.xml.XmlSuite;
25+
import org.testng.xml.XmlSuite.ParallelMode;
2226
import test.SimpleBaseTest;
2327
import test.listeners.issue2381.FactoryTestClassSample;
2428
import test.listeners.issue2381.SampleGlobalListener;
@@ -56,6 +60,8 @@
5660
import test.listeners.issue3082.ObjectRepository;
5761
import test.listeners.issue3082.ObjectTrackingMethodListener;
5862
import test.listeners.issue3095.ChildClassSample;
63+
import test.listeners.issue3238.TestClassWithFailingTestMethodSample;
64+
import test.listeners.issue3238.TestClassWithPassingTestMethodSample;
5965

6066
public class ListenersTest extends SimpleBaseTest {
6167
public static final String[] github2638ExpectedList =
@@ -685,6 +691,31 @@ public void ensureInheritanceIsHandledWhenDealingWithListeners() {
685691
assertThat(testng.getStatus()).isZero();
686692
}
687693

694+
@Test(description = "GITHUB-3238")
695+
public void ensureListenerFailureDoesNotBreakTestNG() {
696+
TestNG testng =
697+
create(
698+
TestClassWithFailingTestMethodSample.class, TestClassWithPassingTestMethodSample.class);
699+
testng.setParallel(ParallelMode.CLASSES);
700+
AtomicInteger failingCount = new AtomicInteger(0);
701+
AtomicInteger passingCount = new AtomicInteger(0);
702+
testng.addListener(
703+
new ITestListener() {
704+
@Override
705+
public void onTestFailure(ITestResult result) {
706+
failingCount.incrementAndGet();
707+
}
708+
709+
@Override
710+
public void onTestSuccess(ITestResult result) {
711+
passingCount.incrementAndGet();
712+
}
713+
});
714+
testng.run();
715+
assertThat(failingCount.get()).isEqualTo(1);
716+
assertThat(passingCount.get()).isEqualTo(1);
717+
}
718+
688719
private void setupTest(boolean addExplicitListener) {
689720
TestNG testng = new TestNG();
690721
XmlSuite xmlSuite = createXmlSuite("Xml_Suite");
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package test.listeners.issue3238;
2+
3+
public class BadUtility {
4+
5+
private static final int counter = evaluate();
6+
7+
private static int evaluate() {
8+
throw new RuntimeException("Failed on purpose");
9+
}
10+
11+
public static void doNothing() {}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package test.listeners.issue3238;
2+
3+
import org.testng.ITestListener;
4+
import org.testng.ITestResult;
5+
6+
public class FailureTrackingListener implements ITestListener {
7+
8+
@Override
9+
public void onTestFailure(ITestResult result) {
10+
BadUtility.doNothing();
11+
}
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package test.listeners.issue3238;
2+
3+
import org.testng.Assert;
4+
import org.testng.annotations.Listeners;
5+
import org.testng.annotations.Test;
6+
7+
@Listeners(FailureTrackingListener.class)
8+
public class TestClassWithFailingTestMethodSample {
9+
10+
@Test
11+
public void failingTest() {
12+
System.err.println(":::::");
13+
Assert.fail();
14+
}
15+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package test.listeners.issue3238;
2+
3+
import org.testng.annotations.Test;
4+
5+
public class TestClassWithPassingTestMethodSample {
6+
@Test
7+
public void testMethod() {}
8+
}

0 commit comments

Comments
 (0)