Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/TUnit.Engine/Framework/TUnitTestFramework.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Testing.Extensions.TrxReport.Abstractions;
using Microsoft.Testing.Platform.Capabilities.TestFramework;
using Microsoft.Testing.Platform.Extensions;
using Microsoft.Testing.Platform.Extensions.Messages;
Expand Down Expand Up @@ -143,9 +144,10 @@ private async Task ReportUnhandledException(ExecuteRequestContext context, Excep
// Same IDE fold as TUnitMessageBus.GetFailureStateProperty: server-mode clients only see
// Exception.Message and Exception.StackTrace, so a discovery hook failure would otherwise
// lose its cause (#1327).
var folded = FlattenedException.Wrap(exception);
var reported = _frameworkServiceProvider.IsConsoleClient()
? exception
: FlattenedException.Wrap(exception);
: folded;

await context.MessageBus.PublishAsync(
dataProducer: this,
Expand All @@ -155,7 +157,13 @@ await context.MessageBus.PublishAsync(
{
DisplayName = $"Unhandled exception - {exception.GetType().Name}: {exception.Message}",
Uid = new TestNodeUid(Guid.NewGuid().ToString()),
Properties = new PropertyBag(new ErrorTestNodeStateProperty(reported))
// This synthetic node has no test method metadata. TRX still needs a type name
// to write the report, and an explicit exception property to retain the cause.
Properties = new PropertyBag(
new ErrorTestNodeStateProperty(reported),
new TrxFullyQualifiedTypeNameProperty(typeof(TUnitTestFramework).FullName!),
new TrxExceptionProperty(folded.Message,
string.IsNullOrEmpty(folded.StackTrace) ? null : folded.StackTrace))
}));
}

Expand Down
56 changes: 56 additions & 0 deletions tests/TUnit.Engine.Tests/UnhandledExceptionTrxTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Shouldly;
using TUnit.Engine.Tests.Enums;

namespace TUnit.Engine.Tests;

public class UnhandledExceptionTrxTests(TestMode testMode) : InvokableTestBase(testMode)
{
[Test]
public async Task Passing_Test_Does_Not_Fail_Session_Without_Disposal_OptIn()
{
await RunTestsWithFilter(
"/*/*/UnhandledExceptionTrxTestCases/PassingTest",
[
result => result.ResultSummary.Outcome.ShouldBe("Completed"),
result => result.ResultSummary.Counters.Total.ShouldBe(1),
result => result.ResultSummary.Counters.Passed.ShouldBe(1),
result => result.ResultSummary.Counters.Failed.ShouldBe(0)
],
new RunOptions().WithEnvironmentVariable("TUNIT_TEST_THROW_ON_STATIC_RESOURCE_DISPOSAL", null));
}

[Test]
public async Task Trx_Preserves_Test_Results_And_Shared_Resource_Disposal_Error()
{
await RunTestsWithFilter(
"/*/*/UnhandledExceptionTrxTestCases/*",
[
result => result.ResultSummary.Outcome.ShouldBe("Failed"),
result => result.ResultSummary.Counters.Total.ShouldBe(3),
result => result.ResultSummary.Counters.Passed.ShouldBe(1),
result => result.ResultSummary.Counters.Failed.ShouldBe(2),
result => result.Results.Single(x => x.TestName == "PassingTest").Outcome.ShouldBe("Passed"),
result =>
{
var failedTest = result.Results.Single(x => x.TestName == "FailingTest");
failedTest.Outcome.ShouldBe("Failed");
var errorInfo = failedTest.Output?.ErrorInfo;
errorInfo.ShouldNotBeNull();
errorInfo.Message.ShouldContain("Test body failure");
errorInfo.StackTrace.ShouldContain("UnhandledExceptionTrxTestCases.FailingTest()");
},
result =>
{
var cleanup = result.Results.Single(x => x.TestName!.StartsWith("Unhandled exception -", StringComparison.Ordinal));
cleanup.Outcome.ShouldBe("Failed");
var errorInfo = cleanup.Output?.ErrorInfo;
errorInfo.ShouldNotBeNull();
errorInfo.Message.ShouldContain("Shared resource disposal failure");
errorInfo.StackTrace.ShouldContain("ThrowingResource.DisposeAsync()");
}
],
new RunOptions()
.WithArgument("--detailed-stacktrace")
.WithEnvironmentVariable("TUNIT_TEST_THROW_ON_STATIC_RESOURCE_DISPOSAL", "1"));
}
}
41 changes: 41 additions & 0 deletions tests/TUnit.TestProject/UnhandledExceptionTrxTestCases.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace TUnit.TestProject;

// Invoked in a child process by UnhandledExceptionTrxTests: the test results must survive
// a failure disposing a static data source after all tests have finished.
public class UnhandledExceptionTrxTestCases
{
[ClassDataSource<ThrowingResource>(Shared = SharedType.PerTestSession)]
public static ThrowingResource Resource { get; set; } = null!;

[Test]
public void PassingTest()
{
Resource.ArmFailure();
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}

[Test]
public void FailingTest()
{
Resource.ArmFailure();
throw new InvalidOperationException("Test body failure");
}

public class ThrowingResource : IAsyncDisposable
{
private bool _used;

public void ArmFailure() => _used = true;

public ValueTask DisposeAsync()
{
// Static data sources can be initialized even when this class is filtered out.
// Only the regression's child process opts in to the deliberate cleanup failure.
if (_used && Environment.GetEnvironmentVariable("TUNIT_TEST_THROW_ON_STATIC_RESOURCE_DISPOSAL") == "1")
{
throw new InvalidOperationException("Shared resource disposal failure");
}

return default;
}
}
}
Loading