Skip to content

Commit 925cdab

Browse files
committed
feat(test): DumpCapture that captures memory dump on test failure
clean up ci dumpcapture code docs
1 parent 85f0a07 commit 925cdab

File tree

4 files changed

+138
-25
lines changed

4 files changed

+138
-25
lines changed

.config/dotnet-tools.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,22 @@
66
"version": "1.10.175",
77
"commands": [
88
"dotnet-serve"
9-
]
9+
],
10+
"rollForward": false
1011
},
1112
"docfx": {
1213
"version": "2.76.0",
1314
"commands": [
1415
"docfx"
15-
]
16+
],
17+
"rollForward": false
18+
},
19+
"dotnet-dump": {
20+
"version": "8.0.532401",
21+
"commands": [
22+
"dotnet-dump"
23+
],
24+
"rollForward": false
1625
}
1726
}
1827
}

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,15 @@ jobs:
136136
8.0.x
137137
9.0.x
138138
139-
- name: ⚙️ Setup GIT versioning
139+
- name: ⚙️ Restore packages and tools
140140
run: |
141141
dotnet restore
142142
dotnet tool restore
143143
144144
- name: 🧪 Run unit tests
145145
run: dotnet test -c release --no-restore -p:VSTestUseMSBuildOutput=false --logger "trx"
146146

147-
- name: Test Report
147+
- name: 🛒 Test Report
148148
uses: dorny/test-reporter@v1
149149
if: success() || failure()
150150
with:

tests/bunit.testassets/DumpCapture.cs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using System.Diagnostics;
2+
using System.Runtime.CompilerServices;
3+
using Xunit.Abstractions;
4+
5+
namespace Bunit.TestAssets;
6+
7+
/// <summary>
8+
/// Wrap a test action or function in a try-catch block that captures a dump file if the test fails.
9+
/// </summary>
10+
/// <remarks>
11+
/// This requires the <c>dotnet-dump</c> tool to be installed as a local dotnet tool.
12+
/// </remarks>
13+
public static class DumpCapture
14+
{
15+
16+
public static async Task OnFailureAsync(
17+
Action testAction,
18+
ITestOutputHelper outputHelper,
19+
[CallerMemberName] string testName = "",
20+
[CallerFilePath] string testFilePath = "")
21+
{
22+
try
23+
{
24+
testAction();
25+
}
26+
catch
27+
{
28+
await CaptureDump(testName, testFilePath, outputHelper);
29+
throw;
30+
}
31+
}
32+
33+
public static async Task OnFailureAsync(
34+
Func<Task> testAction,
35+
ITestOutputHelper outputHelper,
36+
[CallerMemberName] string testName = "",
37+
[CallerFilePath] string testFilePath = "")
38+
{
39+
try
40+
{
41+
await testAction();
42+
}
43+
catch
44+
{
45+
await CaptureDump(testName, testFilePath, outputHelper);
46+
throw;
47+
}
48+
}
49+
50+
private static async Task CaptureDump(string testName, string testFilePath, ITestOutputHelper outputHelper)
51+
{
52+
#if NETSTANDARD2_1
53+
var processId = Process.GetCurrentProcess().Id;
54+
#else
55+
var processId = Environment.ProcessId;
56+
#endif
57+
var dumpFilePath = Path.Combine(Directory.GetCurrentDirectory(), $"{Path.GetFileNameWithoutExtension(testFilePath)}-{testName}-wait-failed-{Guid.NewGuid()}.dmp");
58+
// Attempt to start the dotnet-dump process
59+
var startInfo = new ProcessStartInfo
60+
{
61+
FileName = "dotnet",
62+
Arguments = $"dotnet-dump collect -p {processId} -o {dumpFilePath}",
63+
RedirectStandardOutput = true,
64+
RedirectStandardError = true,
65+
UseShellExecute = false,
66+
CreateNoWindow = true
67+
};
68+
using var process = Process.Start(startInfo);
69+
if (process is null)
70+
{
71+
outputHelper.WriteLine(" Failed to start dotnet-dump process.");
72+
return;
73+
}
74+
75+
#if NETSTANDARD2_1
76+
process.WaitForExit();
77+
#else
78+
await process.WaitForExitAsync();
79+
#endif
80+
var output = await process.StandardOutput.ReadToEndAsync();
81+
var error = await process.StandardError.ReadToEndAsync();
82+
outputHelper.WriteLine($"Dump status: {{process.ExitCode}}. Dump file: {dumpFilePath}");
83+
if (!string.IsNullOrWhiteSpace(output))
84+
{
85+
outputHelper.WriteLine($"Dump output: {output}");
86+
}
87+
if (!string.IsNullOrWhiteSpace(error))
88+
{
89+
outputHelper.WriteLine($"Dump error: {error}");
90+
}
91+
}
92+
}
Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,47 @@
1+
@using Bunit.TestAssets
12
@using Bunit.TestAssets.SampleComponents
23
@inherits TestContext
3-
44
@code {
5+
private readonly ITestOutputHelper outputHelper;
6+
7+
public MarkupMatchesTests(ITestOutputHelper outputHelper)
8+
{
9+
this.outputHelper = outputHelper;
10+
}
511

6-
[Fact]
7-
public void MarkupMatchesShouldNotBeBlockedByRenderer()
8-
{
9-
var tcs = new TaskCompletionSource<object?>();
12+
[Fact]
13+
public async Task MarkupMatchesShouldNotBeBlockedByRenderer()
14+
{
15+
await DumpCapture.OnFailureAsync(() =>
16+
{
17+
var tcs = new TaskCompletionSource<object?>();
1018

11-
var cut = Render(@<LoadingComponent Task="@tcs.Task"/> );
19+
var cut = Render(@<LoadingComponent Task="@tcs.Task" /> );
1220

13-
cut.MarkupMatches(@<span>loading</span>);
21+
cut.MarkupMatches(@<span>loading</span> );
1422

15-
tcs.SetResult(true);
23+
tcs.SetResult(true);
1624

17-
cut.WaitForAssertion(() => cut.MarkupMatches(@<span>done</span>));
18-
}
25+
cut.WaitForAssertion(() => cut.MarkupMatches(@<span>done</span>));
26+
},
27+
outputHelper);
28+
}
1929

20-
[SuppressMessage("Usage", "xUnit1026:Theory method does not use parameter")]
21-
[Theory]
22-
[Repeat(2)]
23-
public void MarkupMatchesShouldNotBeBlockedByRendererComplex(int repeatCount)
24-
{
25-
var tcs = new TaskCompletionSource<object?>();
30+
[Fact]
31+
public async Task MarkupMatchesShouldNotBeBlockedByRendererComplex()
32+
{
33+
await DumpCapture.OnFailureAsync(() =>
34+
{
35+
var tcs = new TaskCompletionSource<object?>();
2636

27-
var cut = Render(@<InvokeAsyncInsideContinueWith Task="@tcs.Task"/> );
37+
var cut = Render(@<InvokeAsyncInsideContinueWith Task="@tcs.Task" /> );
2838

29-
cut.MarkupMatches(@<span>waiting</span>);
39+
cut.MarkupMatches(@<span>waiting</span>);
3040

31-
tcs.SetResult(true);
41+
tcs.SetResult(true);
3242

33-
cut.WaitForAssertion(() => cut.MarkupMatches(@<span>done</span>));
34-
}
43+
cut.WaitForAssertion(() => cut.MarkupMatches(@<span>done</span>));
44+
},
45+
outputHelper);
46+
}
3547
}

0 commit comments

Comments
 (0)