Skip to content

Conversation

@sanych-sun
Copy link
Member

No description provided.

@sanych-sun sanych-sun requested a review from a team as a code owner November 27, 2025 23:42
@sanych-sun sanych-sun added the chore Non–user-facing code changes (tests, build scripts, etc.). label Nov 27, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements tracking for unobserved task exceptions during test execution to help identify tasks that complete with exceptions without proper handling. The implementation uses xUnit's extensibility to register a global handler that captures unobserved exceptions and verifies none occurred after all tests complete.

Key Changes

  • Added custom xUnit test case and discoverer infrastructure to track UnobservedTaskException events globally
  • Implemented a test that runs after all other tests to verify no unobserved exceptions were raised
  • Fixed a race condition in CSharp3302Tests where an unhandled task was causing unobserved exceptions

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
UnobservedExceptionTrackingTestCase.cs New test case class that registers/unregisters the UnobservedTaskException handler and stores exceptions in a static list
UnobservedExceptionTestDiscoverer.cs New xUnit test discoverer and fact attribute to enable unobserved exception tracking
TimeoutEnforcingXunitTestAssemblyRunner.cs Modified to run the unobserved exception tracking test after all other tests complete
UnobservedTaskExceptionTracking.cs New test class that verifies no unobserved exceptions were captured during test execution
CSharp3302Tests.cs Fixed to properly handle task cancellation to prevent unobserved exceptions
Cluster.cs Added disposal check to prevent ObjectDisposedException from timer callback

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


public sealed class UnobservedExceptionTrackingTestCase : XunitTestCase
{
public static readonly List<string> __unobservedExceptions = new();
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The field name __unobservedExceptions uses double underscore prefix which is unconventional for C# public fields. Consider using standard PascalCase naming like UnobservedExceptions or if it needs to remain internal/special, a single underscore prefix like _unobservedExceptions with appropriate access modifier.

Copilot uses AI. Check for mistakes.

public sealed class UnobservedExceptionTrackingTestCase : XunitTestCase
{
public static readonly List<string> __unobservedExceptions = new();
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The static __unobservedExceptions list is not thread-safe. Since xUnit can run tests in parallel, multiple threads may attempt to add to this list simultaneously, causing potential race conditions. Consider using ConcurrentBag<string> or ConcurrentQueue<string> instead of List<string>.

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +42
TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionEventHandler;
}

public override void Dispose()
{
base.Dispose();
TaskScheduler.UnobservedTaskException -= UnobservedTaskExceptionEventHandler;
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UnobservedTaskException handler is registered in the constructor of each test case instance, potentially leading to multiple registrations of the same handler. Since the handler writes to a static list, this creates duplicate tracking. The handler should be registered once globally, not per test case instance.

Suggested change
TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionEventHandler;
}
public override void Dispose()
{
base.Dispose();
TaskScheduler.UnobservedTaskException -= UnobservedTaskExceptionEventHandler;
}
static UnobservedExceptionTrackingTestCase()
{
TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionEventHandler;
}
public override void Dispose()
{
base.Dispose();

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +49
{
return new TimeoutEnforcingXunitTestCollectionRunner(testCollection, testCases, DiagnosticMessageSink, messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync();
}
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The formatting change converts this to a multi-line method but the actual logic is still a single return statement. Consider keeping the expression-bodied member syntax => for consistency with the codebase style, or add meaningful logic to justify the block body.

Suggested change
{
return new TimeoutEnforcingXunitTestCollectionRunner(testCollection, testCases, DiagnosticMessageSink, messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync();
}
=> new TimeoutEnforcingXunitTestCollectionRunner(testCollection, testCases, DiagnosticMessageSink, messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync();

Copilot uses AI. Check for mistakes.
@sanych-sun sanych-sun requested review from BorisDog and removed request for adelinowona November 27, 2025 23:44
{
if (--_serverSelectionWaitQueueSize == 0)
{
if (_disposed)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it better to have try-catch with catching ObjectDisposed ?
Checking for _disposed is still racy.


public sealed class UnobservedExceptionTrackingTestCase : XunitTestCase
{
public static readonly List<string> __unobservedExceptions = new();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be threadsafe?
Also consider removing UnobservedExceptionTrackingTestCase.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not run tests in parallel, at least for now. But probably it makes sense to use something from thread-safe collections.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering whether exceptions can be thrown for different threads?
Or is UnobservedTaskExceptionEventHandler guaranteed to be called from a single thread?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, it will be executed on different threads. Sure will use ConcurrentBag instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore Non–user-facing code changes (tests, build scripts, etc.).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants