-
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 1 commit
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 |
|---|---|---|
|
|
@@ -62,6 +62,12 @@ class TestInvoker extends BaseInvoker implements ITestInvoker { | |
| private final List<IClassListener> m_classListeners; | ||
| private final boolean m_skipFailedInvocationCounts; | ||
|
|
||
| // ThreadLocal to track if we're currently inside runTestResultListener() | ||
| // Used to prevent duplicate listener invocations when a listener throws an exception. | ||
| // See GITHUB-3238. | ||
| private static final ThreadLocal<Boolean> isInvokingListeners = | ||
| ThreadLocal.withInitial(() -> false); | ||
|
|
||
| public TestInvoker( | ||
| ITestResultNotifier m_notifier, | ||
| ITestContext m_testContext, | ||
|
|
@@ -293,9 +299,13 @@ public void runTestResultListener(ITestResult tr) { | |
| m_notifier.getTestListeners(), m_configuration.getListenerComparator()) | ||
| : ListenerOrderDeterminer.order( | ||
| m_notifier.getTestListeners(), m_configuration.getListenerComparator()); | ||
| isInvokingListeners.set(true); | ||
| TestListenerHelper.runTestListeners(tr, listeners); | ||
| TestListenerHelper.runTestListeners( | ||
| tr, Collections.singletonList(m_notifier.getExitCodeListener())); | ||
| // Only reset the flag on successful completion. If an exception is thrown, | ||
| // we want the flag to remain true so the catch block can detect it. | ||
| isInvokingListeners.set(false); | ||
| } | ||
|
|
||
| private Collection<IDataProviderListener> dataProviderListeners() { | ||
|
|
@@ -1020,7 +1030,16 @@ public int invoke(int invCount) { | |
| .ifPresent(it -> TestResult.copyAttributes(it, r)); | ||
| r.setStatus(TestResult.FAILURE); | ||
| result.add(r); | ||
| runTestResultListener(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); | ||
| } | ||
|
Comment on lines
+1033
to
+1039
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. 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
Member
Author
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. 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 commentThe reason will be displayed to describe this comment to others. Learn more.
The test case 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. |
||
| m_notifier.addFailedTest(arguments.getTestMethod(), r); | ||
| } // catch | ||
| return invocationCount.get(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,7 +31,7 @@ protected void done() { | |
| } catch (InterruptedException | ExecutionException e) { | ||
| throwable = e; | ||
| } | ||
| callback.accept(result, throwable); | ||
| callback.accept(result == null ? worker : result, throwable); | ||
|
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. result is null when throwable is not null.
Member
Author
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. fixed |
||
| } | ||
|
|
||
| @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,15 @@ | ||
| 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() { | ||
| System.err.println(":::::"); | ||
| Assert.fail(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package test.listeners.issue3238; | ||
|
|
||
| import org.testng.annotations.Test; | ||
|
|
||
| public class TestClassWithPassingTestMethodSample { | ||
| @Test | ||
| public void testMethod() {} | ||
| } |
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.
Why not always set to false after calling listener?
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.
fixed