-
Notifications
You must be signed in to change notification settings - Fork 1k
Streamline listener invocation handling exceptions #3258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| import java.util.Comparator; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
| import java.util.concurrent.*; | ||
| import org.testng.IDynamicGraph; | ||
| import org.testng.collections.Maps; | ||
|
|
@@ -73,7 +74,8 @@ private void mapNodeToParent(List<T> freeNodes) { | |
| } | ||
| } | ||
|
|
||
| private void afterExecute(IWorker<T> r, Throwable t) { | ||
| private void afterExecute(IWorker<T> original, IWorker<T> result) { | ||
| IWorker<T> r = Optional.ofNullable(result).orElse(original); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What should be done in case of error? Why do we hide it and use original worker?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Original could be captured L59 |
||
| try (AutoCloseableLock ignore = internalLock.lock()) { | ||
| setStatus(r, computeStatus(r)); | ||
| if (graph.getNodeCount() == graph.getNodeCountWithStatus(IDynamicGraph.Status.FINISHED)) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,9 +9,9 @@ | |
| public class TestNGFutureTask<T> extends FutureTask<IWorker<T>> implements IWorker<T> { | ||
|
|
||
| private final IWorker<T> worker; | ||
| private final BiConsumer<IWorker<T>, Throwable> callback; | ||
| private final BiConsumer<IWorker<T>, IWorker<T>> callback; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Callback pattern is result or error, not result or fallback. |
||
|
|
||
| public TestNGFutureTask(IWorker<T> worker, BiConsumer<IWorker<T>, Throwable> callback) { | ||
| public TestNGFutureTask(IWorker<T> worker, BiConsumer<IWorker<T>, IWorker<T>> callback) { | ||
| super(worker, worker); | ||
| this.callback = callback; | ||
| this.worker = worker; | ||
|
|
@@ -24,14 +24,13 @@ public void run() { | |
|
|
||
| @Override | ||
| protected void done() { | ||
| Throwable throwable = null; | ||
| IWorker<T> result = null; | ||
| try { | ||
| result = super.get(); | ||
| } catch (InterruptedException | ExecutionException e) { | ||
| throwable = e; | ||
| // Gobble exception and do nothing. | ||
| } | ||
| callback.accept(result, throwable); | ||
| callback.accept(worker, result); | ||
| } | ||
|
|
||
| @Override | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package test.listeners.issue3238; | ||
|
|
||
| public class BadUtility { | ||
|
|
||
| private static final int counter = evaluate(); | ||
|
|
||
| private static int evaluate() { | ||
| throw new RuntimeException("Failed on purpose"); | ||
| } | ||
|
|
||
| public static void doNothing() {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package test.listeners.issue3238; | ||
|
|
||
| import org.testng.ITestListener; | ||
| import org.testng.ITestResult; | ||
|
|
||
| public class FailureTrackingListener implements ITestListener { | ||
|
|
||
| @Override | ||
| public void onTestFailure(ITestResult result) { | ||
| BadUtility.doNothing(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package test.listeners.issue3238; | ||
|
|
||
| import org.testng.Assert; | ||
| import org.testng.annotations.Listeners; | ||
| import org.testng.annotations.Test; | ||
|
|
||
| @Listeners(FailureTrackingListener.class) | ||
| public class TestClassWithFailingTestMethodSample { | ||
|
|
||
| @Test | ||
| public void failingTest() { | ||
| Assert.fail(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package test.listeners.issue3238; | ||
|
|
||
| import org.testng.annotations.DataProvider; | ||
| import org.testng.annotations.Test; | ||
|
|
||
| public class TestClassWithPassingTestMethodSample { | ||
| @Test | ||
| public void testMethod() {} | ||
|
|
||
| @Test | ||
| public void anotherTestMethod() {} | ||
|
|
||
| @Test(dataProvider = "dp") | ||
| public void dataDrivenTest(int ignored) {} | ||
|
|
||
| @DataProvider(name = "dp", parallel = true) | ||
| public Object[][] dp() { | ||
| return new Object[][] {{1}, {2}, {3}}; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider logging or propagating the listener exception rather than silently skipping.
Per the reviewer's concern, listeners are expected not to throw exceptions, but when they do, the exception should cause the run to fail rather than being silently handled. The current approach skips the listener re-invocation and resets the flag, but the original exception context may be lost.
Consider logging the exception at minimum, or re-throwing it after cleanup to ensure the failure is visible:
💡 Suggested improvement to log or propagate the exception
} catch (Throwable cause) { ITestResult r = TestResult.newEndTimeAwareTestResult( arguments.getTestMethod(), m_testContext, cause, start); Optional.ofNullable(Reporter.getCurrentTestResult()) .ifPresent(it -> TestResult.copyAttributes(it, r)); r.setStatus(TestResult.FAILURE); result.add(r); // Only invoke listeners if the exception didn't come from a listener invocation. // If we're inside a listener invocation, the exception came from a listener, // so don't invoke listeners again to prevent duplicate invocations. // See GITHUB-3238. if (!isInvokingListeners.get()) { runTestResultListener(r); } else { // Reset the flag since we're handling the listener exception here isInvokingListeners.set(false); + // Log warning that a listener threw an exception + log(1, "Listener threw an exception: " + cause.getMessage()); } m_notifier.addFailedTest(arguments.getTestMethod(), r); } // catch🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are only ensuring that we dont invoke the listener twice during failures (wherein a failure could have happened because of the test method or because of the listener itself). There are unit tests added as part of this PR that validate the usecases and ensures that the expected behavior is ascertained.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@krmahadevan, thank you for the clarification. You're right—I can see now that the exception is properly captured in theITestResultobject and recorded viam_notifier.addFailedTest(arguments.getTestMethod(), r), so the failure is not being lost. The code is specifically preventing duplicate listener invocations (which could lead to infinite recursion or double reporting), while still preserving the original exception context in the test result.The test case
ensureListenerFailureDoesNotBreakTestNGwithFailureTrackingListenerandBadUtilityvalidates this scenario effectively. The approach ensures that when a listener throws during parallel execution, the run completes properly without hanging, and the failure is recorded—which aligns with the fix objectives for issue#3238.My concern about "silently handling" was addressed by your explanation. The failure is indeed being reported, just not re-notified to listeners to avoid the problematic double invocation.