Skip to content

Commit 567b4f9

Browse files
ericstjCopilot
andcommitted
Add instrumented xUnit DLLs and diagnostic tracing for RunTest hang
TEMPORARY diagnostic instrumentation to capture what happens during the intermittent xUnit v3 RunTest hang where finished TCS is never signaled. Changes: - Instrumented xunit.v3.core.dll with trace points in TestRunner.RunTest: before/after EC.Run, runTest entry, pre-try completion, test invocation, and finished signaling. Also moves pre-try code inside try block as a structural fix. - DiagnosticExceptionTracing.cs module initializer hooking UnhandledException and UnobservedTaskException for additional context. - Both test csproj files copy instrumented DLLs post-build via ReplaceXunitWithInstrumented target. - CI workflow collects xunit-runtest-diag.log as test artifact. All instrumentation writes to xunit-runtest-diag.log and stderr. Remove once the root cause is identified. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6db87b9 commit 567b4f9

6 files changed

Lines changed: 81 additions & 1 deletion

File tree

.github/workflows/ci-build-test.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ jobs:
8484
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
8585
with:
8686
name: testresults-${{ matrix.os }}-${{ matrix.configuration }}
87-
path: artifacts/testresults/**
87+
path: |
88+
artifacts/testresults/**
89+
artifacts/bin/*Tests/*/xunit-runtest-diag.log
8890
8991
publish-coverage:
9092
if: github.actor != 'dependabot[bot]'
175 KB
Binary file not shown.
477 KB
Binary file not shown.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#if NET
2+
using System.Diagnostics;
3+
using System.Runtime.CompilerServices;
4+
using System.Runtime.ExceptionServices;
5+
6+
namespace ModelContextProtocol.Tests.Utils;
7+
8+
/// <summary>
9+
/// Module initializer that hooks global exception events to diagnose async void crashes
10+
/// and other unobserved exceptions during test execution. This is intended to capture
11+
/// evidence for intermittent xUnit test runner hangs where an async void method's exception
12+
/// may go unobserved, preventing a TaskCompletionSource from being signaled.
13+
/// </summary>
14+
internal static class DiagnosticExceptionTracing
15+
{
16+
private static int s_initialized;
17+
private static int s_unhandledExceptionCount;
18+
private static int s_unobservedTaskExceptionCount;
19+
20+
[ModuleInitializer]
21+
internal static void Initialize()
22+
{
23+
if (Interlocked.Exchange(ref s_initialized, 1) != 0)
24+
{
25+
return;
26+
}
27+
28+
// This fires when an async void method throws and the exception propagates to the
29+
// thread pool. If the xUnit runTest async void function throws before its try block,
30+
// we'll see it here. Note: this typically terminates the process, so seeing output
31+
// from this handler is strong evidence of the suspected bug.
32+
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
33+
{
34+
int count = Interlocked.Increment(ref s_unhandledExceptionCount);
35+
string terminating = e.IsTerminating ? "TERMINATING" : "non-terminating";
36+
Console.Error.WriteLine($"[DiagnosticExceptionTracing] UnhandledException #{count} ({terminating}): {e.ExceptionObject}");
37+
Console.Error.Flush();
38+
39+
// Also write to Trace in case stderr is not captured
40+
Trace.WriteLine($"[DiagnosticExceptionTracing] UnhandledException #{count} ({terminating}): {e.ExceptionObject}");
41+
};
42+
43+
// This fires when a Task's exception is never observed (no await, no Wait, no Result).
44+
// Could indicate fire-and-forget tasks with silent failures.
45+
TaskScheduler.UnobservedTaskException += (sender, e) =>
46+
{
47+
int count = Interlocked.Increment(ref s_unobservedTaskExceptionCount);
48+
Console.Error.WriteLine($"[DiagnosticExceptionTracing] UnobservedTaskException #{count}: {e.Exception}");
49+
Console.Error.Flush();
50+
};
51+
52+
Console.Error.WriteLine("[DiagnosticExceptionTracing] Exception tracing hooks installed");
53+
Console.Error.Flush();
54+
}
55+
}
56+
#endif

tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,15 @@
6262
<ProjectReference Include="..\ModelContextProtocol.ConformanceServer\ModelContextProtocol.ConformanceServer.csproj" />
6363
</ItemGroup>
6464

65+
<!-- TEMPORARY: Replace xUnit DLLs with instrumented versions that add tracing to
66+
TestRunner.RunTest to diagnose intermittent hang where finished TCS is never signaled.
67+
Remove this target once the root cause is identified. -->
68+
<Target Name="ReplaceXunitWithInstrumented" AfterTargets="Build"
69+
Condition="Exists('$(MSBuildThisFileDirectory)..\Common\InstrumentedXunit\xunit.v3.core.dll')">
70+
<Copy SourceFiles="$(MSBuildThisFileDirectory)..\Common\InstrumentedXunit\xunit.v3.core.dll"
71+
DestinationFolder="$(OutputPath)" OverwriteReadOnlyFiles="true" SkipUnchangedFiles="false" />
72+
<Copy SourceFiles="$(MSBuildThisFileDirectory)..\Common\InstrumentedXunit\xunit.v3.common.dll"
73+
DestinationFolder="$(OutputPath)" OverwriteReadOnlyFiles="true" SkipUnchangedFiles="false" />
74+
</Target>
75+
6576
</Project>

tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,15 @@
9898
</Content>
9999
</ItemGroup>
100100

101+
<!-- TEMPORARY: Replace xUnit DLLs with instrumented versions that add tracing to
102+
TestRunner.RunTest to diagnose intermittent hang where finished TCS is never signaled.
103+
Remove this target once the root cause is identified. -->
104+
<Target Name="ReplaceXunitWithInstrumented" AfterTargets="Build"
105+
Condition="Exists('$(MSBuildThisFileDirectory)..\Common\InstrumentedXunit\xunit.v3.core.dll')">
106+
<Copy SourceFiles="$(MSBuildThisFileDirectory)..\Common\InstrumentedXunit\xunit.v3.core.dll"
107+
DestinationFolder="$(OutputPath)" OverwriteReadOnlyFiles="true" SkipUnchangedFiles="false" />
108+
<Copy SourceFiles="$(MSBuildThisFileDirectory)..\Common\InstrumentedXunit\xunit.v3.common.dll"
109+
DestinationFolder="$(OutputPath)" OverwriteReadOnlyFiles="true" SkipUnchangedFiles="false" />
110+
</Target>
111+
101112
</Project>

0 commit comments

Comments
 (0)