diff --git a/src/MongoDB.Driver/Core/Clusters/Cluster.cs b/src/MongoDB.Driver/Core/Clusters/Cluster.cs index db14eed8b19..fba95fb5d81 100644 --- a/src/MongoDB.Driver/Core/Clusters/Cluster.cs +++ b/src/MongoDB.Driver/Core/Clusters/Cluster.cs @@ -489,7 +489,14 @@ private void ExitServerSelectionWaitQueue() { if (--_serverSelectionWaitQueueSize == 0) { - _rapidHeartbeatTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + try + { + _rapidHeartbeatTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + } + catch (ObjectDisposedException) + { + // Ignore ObjectDisposedException here, as ExitServerSelectionWaitQueue could be done after the WaitQueue was disposed. + } } } } diff --git a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs index db1e71df9ca..78f7fcc09f1 100644 --- a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs +++ b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs @@ -165,42 +165,54 @@ private void ConfigureConnectedSocket(Socket socket) private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancellationToken) { - IAsyncResult connectOperation; -#if NET472 - if (endPoint is DnsEndPoint dnsEndPoint) - { - // mono doesn't support DnsEndPoint in its BeginConnect method. - connectOperation = socket.BeginConnect(dnsEndPoint.Host, dnsEndPoint.Port, null, null); - } - else + var isSocketDisposed = false; + using var timeoutCancellationTokenSource = new CancellationTokenSource(_settings.ConnectTimeout); + using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token); + using var cancellationSubscription = combinedCancellationTokenSource.Token.Register(DisposeSocket); + + try { - connectOperation = socket.BeginConnect(endPoint, null, null); - } +#if NET472 + if (endPoint is DnsEndPoint dnsEndPoint) + { + // mono doesn't support DnsEndPoint in its Connect method. + socket.Connect(dnsEndPoint.Host, dnsEndPoint.Port); + } + else + { + socket.Connect(endPoint); + } #else - connectOperation = socket.BeginConnect(endPoint, null, null); + socket.Connect(endPoint); #endif - - WaitHandle.WaitAny([connectOperation.AsyncWaitHandle, cancellationToken.WaitHandle], _settings.ConnectTimeout); - - if (!connectOperation.IsCompleted) + } + catch { - try + if (!isSocketDisposed) { - socket.Dispose(); - } catch { } + DisposeSocket(); + } cancellationToken.ThrowIfCancellationRequested(); - throw new TimeoutException($"Timed out connecting to {endPoint}. Timeout was {_settings.ConnectTimeout}."); - } + if (timeoutCancellationTokenSource.IsCancellationRequested) + { + throw new TimeoutException($"Timed out connecting to {endPoint}. Timeout was {_settings.ConnectTimeout}."); + } - try - { - socket.EndConnect(connectOperation); + throw; } - catch + + void DisposeSocket() { - try { socket.Dispose(); } catch { } - throw; + isSocketDisposed = true; + try + { + socket.Dispose(); + } + catch + { + // Ignore any exceptions. + } } } @@ -228,8 +240,8 @@ private async Task ConnectAsync(Socket socket, EndPoint endPoint, CancellationTo { try { - socket.Dispose(); connectTask.IgnoreExceptions(); + socket.Dispose(); } catch { } diff --git a/tests/MongoDB.Bson.Tests/UnobservedTaskExceptionTracking.cs b/tests/MongoDB.Bson.Tests/UnobservedTaskExceptionTracking.cs new file mode 100644 index 00000000000..1d535659b3c --- /dev/null +++ b/tests/MongoDB.Bson.Tests/UnobservedTaskExceptionTracking.cs @@ -0,0 +1,33 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using FluentAssertions; +using MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing; + +namespace MongoDB.Bson.Tests; + +public class UnobservedTaskExceptionTracking +{ + [UnobservedExceptionTrackingFact] + public void EnsureNoUnobservedTaskException() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + + UnobservedExceptionTestDiscoverer.UnobservedExceptions.Should().BeEmpty(); + } +} + diff --git a/tests/MongoDB.Driver.Tests/ClusterRegistryTests.cs b/tests/MongoDB.Driver.Tests/ClusterRegistryTests.cs index 2a63791de2f..666f42ddd92 100644 --- a/tests/MongoDB.Driver.Tests/ClusterRegistryTests.cs +++ b/tests/MongoDB.Driver.Tests/ClusterRegistryTests.cs @@ -27,7 +27,6 @@ using MongoDB.Driver.Core.Misc; using MongoDB.Driver.Core.Servers; using MongoDB.Driver.Core.TestHelpers.Logging; -using MongoDB.Driver.TestHelpers; using Xunit; using Xunit.Abstractions; @@ -73,7 +72,6 @@ public void DisposingClusterSource_should_use_cluster_registry_and_return_cluste ClusterRegistry.Instance._registry().Keys.Should().NotContain(clusterKey); } -#if WINDOWS [Fact] public void Instance_should_return_the_same_instance_every_time() { @@ -221,7 +219,6 @@ public void UnregisterAndDisposeCluster_should_unregister_and_dispose_the_cluste subject._registry().Count.Should().Be(0); cluster._state().Should().Be(2); } -#endif } internal static class ClusterRegistryReflector diff --git a/tests/MongoDB.Driver.Tests/Core/Connections/TcpStreamFactoryTests.cs b/tests/MongoDB.Driver.Tests/Core/Connections/TcpStreamFactoryTests.cs index cfb8f8194c5..5a11faba3b1 100644 --- a/tests/MongoDB.Driver.Tests/Core/Connections/TcpStreamFactoryTests.cs +++ b/tests/MongoDB.Driver.Tests/Core/Connections/TcpStreamFactoryTests.cs @@ -34,7 +34,7 @@ public class TcpStreamFactoryTests { [Theory] [ParameterAttributeData] - public void Connect_should_dispose_socket_if_socket_fails([Values(false, true)] bool async) + public async Task Connect_should_dispose_socket_if_socket_fails([Values(false, true)] bool async) { RequireServer.Check(); @@ -43,20 +43,9 @@ public void Connect_should_dispose_socket_if_socket_fails([Values(false, true)] using (var testSocket = new TestSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) { - Exception exception; - if (async) - { - exception = Record.Exception( - () => - subject - .ConnectAsync(testSocket, endpoint, CancellationToken.None) - .GetAwaiter() - .GetResult()); - } - else - { - exception = Record.Exception(() => subject.Connect(testSocket, endpoint, CancellationToken.None)); - } + var exception = async ? + await Record.ExceptionAsync(() => subject.ConnectAsync(testSocket, endpoint, CancellationToken.None)) : + Record.Exception(() => subject.Connect(testSocket, endpoint, CancellationToken.None)); exception.Should().NotBeNull(); testSocket.DisposeAttempts.Should().Be(1); @@ -66,83 +55,64 @@ public void Connect_should_dispose_socket_if_socket_fails([Values(false, true)] [Fact] public void Constructor_should_throw_an_ArgumentNullException_when_tcpStreamSettings_is_null() { - Action act = () => new TcpStreamFactory(null); + var exception = Record.Exception(() => new TcpStreamFactory(null)); - act.ShouldThrow(); + exception.Should().BeOfType().Subject + .ParamName.Should().Be("settings"); } [Theory] [ParameterAttributeData] - public void CreateStream_should_throw_a_SocketException_when_the_endpoint_could_not_be_resolved( - [Values(false, true)] - bool async) + public async Task CreateStream_should_throw_a_SocketException_when_the_endpoint_could_not_be_resolved([Values(false, true)]bool async) { var subject = new TcpStreamFactory(); - Action act; - if (async) - { - act = () => subject.CreateStreamAsync(new DnsEndPoint("not-gonna-exist-i-hope", 27017), CancellationToken.None).GetAwaiter().GetResult(); - } - else - { - act = () => subject.CreateStream(new DnsEndPoint("not-gonna-exist-i-hope", 27017), CancellationToken.None); - } + var exception = async ? + await Record.ExceptionAsync(() => subject.CreateStreamAsync(new DnsEndPoint("not-gonna-exist-i-hope", 27017), CancellationToken.None)) : + Record.Exception(() => subject.CreateStream(new DnsEndPoint("not-gonna-exist-i-hope", 27017), CancellationToken.None)); - act.ShouldThrow(); + exception.Should().BeAssignableTo(); } [Theory] [ParameterAttributeData] - public void CreateStream_should_throw_when_cancellation_is_requested( - [Values(false, true)] - bool async) + public async Task CreateStream_should_throw_when_cancellation_is_requested([Values(false, true)]bool async) { var subject = new TcpStreamFactory(); var endPoint = new IPEndPoint(new IPAddress(0x01010101), 12345); // a non-existent host and port var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(20)); - Action action; + var exception = async ? + await Record.ExceptionAsync(() => subject.CreateStreamAsync(endPoint, cancellationTokenSource.Token)) : + Record.Exception(() => subject.CreateStream(endPoint, cancellationTokenSource.Token)); if (async) { - action = () => subject.CreateStreamAsync(endPoint, cancellationTokenSource.Token).GetAwaiter().GetResult(); + exception.Should().BeOfType(); } else { - action = () => subject.CreateStream(endPoint, cancellationTokenSource.Token); + exception.Should().BeOfType(); } - - action.ShouldThrow(); } [Theory] [ParameterAttributeData] - public void CreateStream_should_throw_when_connect_timeout_has_expired( - [Values(false, true)] - bool async) + public async Task CreateStream_should_throw_when_connect_timeout_has_expired([Values(false, true)]bool async) { var settings = new TcpStreamSettings(connectTimeout: TimeSpan.FromMilliseconds(20)); var subject = new TcpStreamFactory(settings); var endPoint = new IPEndPoint(new IPAddress(0x01010101), 12345); // a non-existent host and port - Action action; - if (async) - { - action = () => subject.CreateStreamAsync(endPoint, CancellationToken.None).GetAwaiter().GetResult(); ; - } - else - { - action = () => subject.CreateStream(endPoint, CancellationToken.None); - } + var exception = async ? + await Record.ExceptionAsync(() => subject.CreateStreamAsync(endPoint, CancellationToken.None)) : + Record.Exception(() => subject.CreateStream(endPoint, CancellationToken.None)); - action.ShouldThrow(); + exception.Should().BeOfType(); } [Theory] [ParameterAttributeData] - public void CreateStream_should_call_the_socketConfigurator( - [Values(false, true)] - bool async) + public async Task CreateStream_should_call_the_socketConfigurator([Values(false, true)]bool async) { RequireServer.Check(); var socketConfiguratorWasCalled = false; @@ -153,7 +123,7 @@ public void CreateStream_should_call_the_socketConfigurator( if (async) { - subject.CreateStreamAsync(endPoint, CancellationToken.None).GetAwaiter().GetResult(); + await subject.CreateStreamAsync(endPoint, CancellationToken.None); } else { @@ -165,9 +135,7 @@ public void CreateStream_should_call_the_socketConfigurator( [Theory] [ParameterAttributeData] - public void CreateStream_should_connect_to_a_running_server_and_return_a_non_null_stream( - [Values(false, true)] - bool async) + public async Task CreateStream_should_connect_to_a_running_server_and_return_a_non_null_stream([Values(false, true)]bool async) { RequireServer.Check(); var subject = new TcpStreamFactory(); @@ -176,7 +144,7 @@ public void CreateStream_should_connect_to_a_running_server_and_return_a_non_nul Stream stream; if (async) { - stream = subject.CreateStreamAsync(endPoint, CancellationToken.None).GetAwaiter().GetResult(); + stream = await subject.CreateStreamAsync(endPoint, CancellationToken.None); } else { @@ -188,9 +156,7 @@ public void CreateStream_should_connect_to_a_running_server_and_return_a_non_nul [Theory] [ParameterAttributeData] - public void SocketConfigurator_can_be_used_to_set_keepAlive( - [Values(false, true)] - bool async) + public async Task SocketConfigurator_can_be_used_to_set_keepAlive([Values(false, true)]bool async) { RequireServer.Check(); Action socketConfigurator = s => s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); @@ -201,7 +167,7 @@ public void SocketConfigurator_can_be_used_to_set_keepAlive( Stream stream; if (async) { - stream = subject.CreateStreamAsync(endPoint, CancellationToken.None).GetAwaiter().GetResult(); + stream = await subject.CreateStreamAsync(endPoint, CancellationToken.None); } else { diff --git a/tests/MongoDB.Driver.Tests/Core/Jira/CSharp3302Tests.cs b/tests/MongoDB.Driver.Tests/Core/Jira/CSharp3302Tests.cs index 792344ad6de..fbd2a379525 100644 --- a/tests/MongoDB.Driver.Tests/Core/Jira/CSharp3302Tests.cs +++ b/tests/MongoDB.Driver.Tests/Core/Jira/CSharp3302Tests.cs @@ -96,10 +96,15 @@ public async Task RapidHeartbeatTimerCallback_should_ignore_reentrant_calls() cluster.Initialize(); // Trigger Cluster._rapidHeartbeatTimer - _ = cluster.SelectServerAsync(OperationContext.NoTimeout, CreateWritableServerAndEndPointSelector(__endPoint1)); + using var cancellationTokenSource = new CancellationTokenSource(); + var operationContext = new OperationContext(Timeout.InfiniteTimeSpan, cancellationTokenSource.Token); + cluster.SelectServerAsync(operationContext, CreateWritableServerAndEndPointSelector(__endPoint1)) + .IgnoreExceptions(); // Wait for all heartbeats to complete await Task.WhenAny(allHeartbeatsReceived.Task, Task.Delay(1000)); + + cancellationTokenSource.Cancel(); } allHeartbeatsReceived.Task.Status.Should().Be(TaskStatus.RanToCompletion); diff --git a/tests/MongoDB.Driver.Tests/Specifications/connection-monitoring-and-pooling/ConnectionMonitoringAndPoolingTestRunner.cs b/tests/MongoDB.Driver.Tests/Specifications/connection-monitoring-and-pooling/ConnectionMonitoringAndPoolingTestRunner.cs index 6da8fcb73af..36167447d7d 100644 --- a/tests/MongoDB.Driver.Tests/Specifications/connection-monitoring-and-pooling/ConnectionMonitoringAndPoolingTestRunner.cs +++ b/tests/MongoDB.Driver.Tests/Specifications/connection-monitoring-and-pooling/ConnectionMonitoringAndPoolingTestRunner.cs @@ -387,6 +387,7 @@ private void ExecuteCheckOut( else { tasks[target] = CreateTask(() => CheckOut(operation, connectionPool, map)); + tasks[target].IgnoreExceptions(); } } } diff --git a/tests/MongoDB.Driver.Tests/UnobservedTaskExceptionTracking.cs b/tests/MongoDB.Driver.Tests/UnobservedTaskExceptionTracking.cs new file mode 100644 index 00000000000..128930c0fbc --- /dev/null +++ b/tests/MongoDB.Driver.Tests/UnobservedTaskExceptionTracking.cs @@ -0,0 +1,42 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using FluentAssertions; +using MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing; +using Xunit; + +namespace MongoDB.Driver.Tests; + +public class UnobservedTaskExceptionTracking +{ + [UnobservedExceptionTrackingFact] + public void EnsureNoUnobservedTaskException() => + EnsureNoUnobservedTaskExceptionImpl(); + + [UnobservedExceptionTrackingFact] + [Trait("Category", "Integration")] + public void EnsureNoUnobservedTaskException_Integration() => + EnsureNoUnobservedTaskExceptionImpl(); + + private void EnsureNoUnobservedTaskExceptionImpl() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + + UnobservedExceptionTestDiscoverer.UnobservedExceptions.Should().BeEmpty(); + } +} + diff --git a/tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj b/tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj index f26db14e0fb..67b38768be9 100644 --- a/tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj +++ b/tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj @@ -12,4 +12,9 @@ Helper classes applicable to all test projects. + + + + + diff --git a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingTestInvoker.cs b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingTestInvoker.cs index 69c16fc24d8..4e6a2c0e43b 100644 --- a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingTestInvoker.cs +++ b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingTestInvoker.cs @@ -90,11 +90,11 @@ protected override async Task InvokeTestMethodAsync(object testClassIns var testExceptionHandler = testClassInstance as ITestExceptionHandler; decimal result; + using var unobservedExceptionDebugger = UnobservedExceptionDebugger.Create(); try { var baseTask = InvokeBaseOnTaskScheduler(testClassInstance); var resultTask = await Task.WhenAny(baseTask, Task.Delay(timeout)); - if (resultTask != baseTask) { throw new TestTimeoutException((int)timeout.TotalMilliseconds); @@ -121,5 +121,41 @@ protected override async Task InvokeTestMethodAsync(object testClassIns return result; } + + private class UnobservedExceptionDebugger : IDisposable + { + private Exception _unobservedException; + + private UnobservedExceptionDebugger() + { + TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionEventHandler; + } + + public static UnobservedExceptionDebugger Create() + { +#if UNOBSERVED_TASK_EXCEPTION_DEBUGGING + return new UnobservedExceptionDebugger(); +#else + return null; +#endif + } + + public void Dispose() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + TaskScheduler.UnobservedTaskException -= UnobservedTaskExceptionEventHandler; + + if (_unobservedException != null) + { + throw _unobservedException; + } + } + + private void UnobservedTaskExceptionEventHandler(object sender, UnobservedTaskExceptionEventArgs unobservedExceptionArgs) + { + _unobservedException = unobservedExceptionArgs.Exception; + } + } } } diff --git a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingXunitTestAssemblyRunner.cs b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingXunitTestAssemblyRunner.cs index 7719fe7500e..29072b20ab9 100644 --- a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingXunitTestAssemblyRunner.cs +++ b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingXunitTestAssemblyRunner.cs @@ -15,6 +15,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Xunit.Abstractions; @@ -25,20 +26,58 @@ namespace MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing [DebuggerStepThrough] internal sealed class TimeoutEnforcingXunitTestAssemblyRunner : XunitTestAssemblyRunner { + private readonly IXunitTestCase _unobservedExceptionTrackingTestCase; + public TimeoutEnforcingXunitTestAssemblyRunner( ITestAssembly testAssembly, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions) - : base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions) - { } + : base( + testAssembly, + testCases.Where(t => !IsUnobservedExceptionTrackingTestCase(t)), + diagnosticMessageSink, + executionMessageSink, + executionOptions) + { + _unobservedExceptionTrackingTestCase = testCases.SingleOrDefault(IsUnobservedExceptionTrackingTestCase); + } protected override Task RunTestCollectionAsync( IMessageBus messageBus, ITestCollection testCollection, IEnumerable testCases, - CancellationTokenSource cancellationTokenSource) => - new TimeoutEnforcingXunitTestCollectionRunner(testCollection, testCases,DiagnosticMessageSink, messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync(); + CancellationTokenSource cancellationTokenSource) + { + return new TimeoutEnforcingXunitTestCollectionRunner(testCollection, testCases, DiagnosticMessageSink, messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync(); + } + + protected override async Task RunTestCollectionsAsync(IMessageBus messageBus, CancellationTokenSource cancellationTokenSource) + { + var baseSummary = await base.RunTestCollectionsAsync(messageBus, cancellationTokenSource); + + if (_unobservedExceptionTrackingTestCase == null) + { + return baseSummary; + } + + var unobservedExceptionTestCaseRunSummary = await RunTestCollectionAsync( + messageBus, + _unobservedExceptionTrackingTestCase.TestMethod.TestClass.TestCollection, + [_unobservedExceptionTrackingTestCase], + cancellationTokenSource); + + return new RunSummary + { + Total = baseSummary.Total + unobservedExceptionTestCaseRunSummary.Total, + Failed = baseSummary.Failed + unobservedExceptionTestCaseRunSummary.Failed, + Skipped = baseSummary.Skipped + unobservedExceptionTestCaseRunSummary.Skipped, + Time = baseSummary.Time + unobservedExceptionTestCaseRunSummary.Time + }; + } + + private static bool IsUnobservedExceptionTrackingTestCase(IXunitTestCase testCase) + => testCase.Traits.TryGetValue("Category", out var categories) && categories.Contains("UnobservedExceptionTracking"); } } diff --git a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs new file mode 100644 index 00000000000..b4fdc098c48 --- /dev/null +++ b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs @@ -0,0 +1,57 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing; + +[XunitTestCaseDiscoverer("MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing.UnobservedExceptionTestDiscoverer", "MongoDB.TestHelpers")] +public class UnobservedExceptionTrackingFactAttribute: FactAttribute +{} + +public class UnobservedExceptionTestDiscoverer : IXunitTestCaseDiscoverer +{ + private static readonly ConcurrentBag __unobservedExceptions = new(); + + private readonly IMessageSink _diagnosticsMessageSink; + + public UnobservedExceptionTestDiscoverer(IMessageSink diagnosticsMessageSink) + { + _diagnosticsMessageSink = diagnosticsMessageSink; + TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionEventHandler; + } + + public static IReadOnlyCollection UnobservedExceptions => __unobservedExceptions; + + public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) + { + return [new XunitTestCase(_diagnosticsMessageSink, TestMethodDisplay.Method, TestMethodDisplayOptions.All, testMethod) + { + Traits = + { + { "Category", ["UnobservedExceptionTracking"] } + } + }]; + } + + void UnobservedTaskExceptionEventHandler(object sender, UnobservedTaskExceptionEventArgs unobservedException) => + __unobservedExceptions.Add(unobservedException.Exception.ToString()); +} +