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
135 changes: 135 additions & 0 deletions TUnit.Core/Exceptions/TestExecutionException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
namespace TUnit.Core.Exceptions;

/// <summary>
/// Exception thrown when one or more failures occur during test execution, including the test itself,
/// hooks (before/after test methods), or event receivers.
/// This exception aggregates multiple failure types to provide comprehensive error reporting.
/// </summary>
/// <remarks>
/// This exception is thrown in the following scenarios:
/// <list type="bullet">
/// <item><description>The test itself fails and one or more hooks or event receivers also fail</description></item>
/// <item><description>The test passes but one or more hooks or event receivers fail during cleanup</description></item>
/// <item><description>Multiple hooks fail during test execution</description></item>
/// <item><description>Multiple event receivers fail during test execution</description></item>
/// </list>
/// The <see cref="InnerException"/> property contains either a single exception or an <see cref="AggregateException"/>
/// when multiple failures occur.
/// </remarks>
public class TestExecutionException : TUnitException
{
/// <summary>
/// Gets the exception thrown by the test itself, or null if the test passed.
/// </summary>
public Exception? TestException { get; }

/// <summary>
/// Gets the collection of exceptions thrown by hooks during test execution.
/// </summary>
public IReadOnlyList<Exception> HookExceptions { get; }

/// <summary>
/// Gets the collection of exceptions thrown by event receivers during test execution.
/// </summary>
public IReadOnlyList<Exception> EventReceiverExceptions { get; }

/// <summary>
/// Initializes a new instance of the <see cref="TestExecutionException"/> class.
/// </summary>
/// <param name="testException">The exception thrown by the test, or null if the test passed.</param>
/// <param name="hookExceptions">The collection of exceptions thrown by hooks.</param>
/// <param name="eventReceiverExceptions">The collection of exceptions thrown by event receivers.</param>
public TestExecutionException(
Exception? testException,
IReadOnlyList<Exception> hookExceptions,
IReadOnlyList<Exception> eventReceiverExceptions)
: base(BuildMessage(testException, hookExceptions, eventReceiverExceptions),
BuildInnerException(testException, hookExceptions, eventReceiverExceptions))
{
TestException = testException;
HookExceptions = hookExceptions;
EventReceiverExceptions = eventReceiverExceptions;
}

private static string BuildMessage(
Exception? testException,
IReadOnlyList<Exception> hookExceptions,
IReadOnlyList<Exception> eventReceiverExceptions)
{
var parts = new List<string>();

if (testException is not null)
{
parts.Add($"Test failed: {testException.Message}");
}

if (hookExceptions.Count > 0)
{
if (hookExceptions.Count == 1)
{
parts.Add(hookExceptions[0].Message);
}
else
{
var messageBuilder = new System.Text.StringBuilder();
messageBuilder.Append("Multiple hooks failed: ");
for (var i = 0; i < hookExceptions.Count; i++)
{
if (i > 0)
{
messageBuilder.Append("; ");
}
messageBuilder.Append(hookExceptions[i].Message);
}
parts.Add(messageBuilder.ToString());
}
}

if (eventReceiverExceptions.Count > 0)
{
if (eventReceiverExceptions.Count == 1)
{
parts.Add($"Test end event receiver failed: {eventReceiverExceptions[0].Message}");
}
else
{
var messageBuilder = new System.Text.StringBuilder();
messageBuilder.Append($"{eventReceiverExceptions.Count} test end event receivers failed: ");
for (var i = 0; i < eventReceiverExceptions.Count; i++)
{
if (i > 0)
{
messageBuilder.Append("; ");
}
messageBuilder.Append(eventReceiverExceptions[i].Message);
}
parts.Add(messageBuilder.ToString());
}
}

return string.Join(" | ", parts);
}

private static Exception? BuildInnerException(
Exception? testException,
IReadOnlyList<Exception> hookExceptions,
IReadOnlyList<Exception> eventReceiverExceptions)
{
var allExceptions = new List<Exception>();

if (testException is not null)
{
allExceptions.Add(testException);
}

allExceptions.AddRange(hookExceptions);
allExceptions.AddRange(eventReceiverExceptions);

return allExceptions.Count switch
{
0 => null,
1 => allExceptions[0],
_ => new AggregateException(allExceptions)
};
}
}
215 changes: 215 additions & 0 deletions TUnit.Engine.Tests/TestExecutionExceptionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
using Shouldly;
using TUnit.Core.Exceptions;

namespace TUnit.Engine.Tests;

public class TestExecutionExceptionTests
{
[Test]
public void TestExecutionException_WithSingleTestException_BuildsCorrectMessage()
{
var testException = new InvalidOperationException("Test failed");

var exception = new TestExecutionException(testException, [], []);

exception.Message.ShouldBe("Test failed: Test failed");
exception.TestException.ShouldBe(testException);
exception.HookExceptions.ShouldBeEmpty();
exception.EventReceiverExceptions.ShouldBeEmpty();
exception.InnerException.ShouldBe(testException);
}

[Test]
public void TestExecutionException_WithSingleHookException_BuildsCorrectMessage()
{
var hookException = new InvalidOperationException("Hook failed");

var exception = new TestExecutionException(null, [hookException], []);

exception.Message.ShouldBe("Hook failed");
exception.TestException.ShouldBeNull();
exception.HookExceptions.Count.ShouldBe(1);
exception.EventReceiverExceptions.ShouldBeEmpty();
exception.InnerException.ShouldBe(hookException);
}

[Test]
public void TestExecutionException_WithMultipleHookExceptions_BuildsCorrectMessage()
{
var hookException1 = new InvalidOperationException("Hook 1 failed");
var hookException2 = new ArgumentException("Hook 2 failed");

var exception = new TestExecutionException(null, [hookException1, hookException2], []);

exception.Message.ShouldBe("Multiple hooks failed: Hook 1 failed; Hook 2 failed");
exception.TestException.ShouldBeNull();
exception.HookExceptions.Count.ShouldBe(2);
exception.EventReceiverExceptions.ShouldBeEmpty();
exception.InnerException.ShouldBeOfType<AggregateException>();
var aggEx = (AggregateException)exception.InnerException!;
aggEx.InnerExceptions.Count.ShouldBe(2);
}

[Test]
public void TestExecutionException_WithSingleEventReceiverException_BuildsCorrectMessage()
{
var receiverException = new InvalidOperationException("Receiver failed");

var exception = new TestExecutionException(null, [], [receiverException]);

exception.Message.ShouldBe("Test end event receiver failed: Receiver failed");
exception.TestException.ShouldBeNull();
exception.HookExceptions.ShouldBeEmpty();
exception.EventReceiverExceptions.Count.ShouldBe(1);
exception.InnerException.ShouldBe(receiverException);
}

[Test]
public void TestExecutionException_WithMultipleEventReceiverExceptions_BuildsCorrectMessage()
{
var receiverException1 = new InvalidOperationException("Receiver 1 failed");
var receiverException2 = new ArgumentException("Receiver 2 failed");
var receiverException3 = new NullReferenceException("Receiver 3 failed");

var exception = new TestExecutionException(null, [], [receiverException1, receiverException2, receiverException3]);

exception.Message.ShouldBe("3 test end event receivers failed: Receiver 1 failed; Receiver 2 failed; Receiver 3 failed");
exception.TestException.ShouldBeNull();
exception.HookExceptions.ShouldBeEmpty();
exception.EventReceiverExceptions.Count.ShouldBe(3);
exception.InnerException.ShouldBeOfType<AggregateException>();
var aggEx = (AggregateException)exception.InnerException!;
aggEx.InnerExceptions.Count.ShouldBe(3);
}

[Test]
public void TestExecutionException_WithTestAndHookExceptions_BuildsCorrectMessage()
{
var testException = new InvalidOperationException("Test failed");
var hookException = new ArgumentException("Hook failed");

var exception = new TestExecutionException(testException, [hookException], []);

exception.Message.ShouldBe("Test failed: Test failed | Hook failed");
exception.TestException.ShouldBe(testException);
exception.HookExceptions.Count.ShouldBe(1);
exception.EventReceiverExceptions.ShouldBeEmpty();
exception.InnerException.ShouldBeOfType<AggregateException>();
var aggEx = (AggregateException)exception.InnerException!;
aggEx.InnerExceptions.Count.ShouldBe(2);
}

[Test]
public void TestExecutionException_WithTestAndMultipleHookExceptions_BuildsCorrectMessage()
{
var testException = new InvalidOperationException("Test failed");
var hookException1 = new ArgumentException("Hook 1 failed");
var hookException2 = new NullReferenceException("Hook 2 failed");

var exception = new TestExecutionException(testException, [hookException1, hookException2], []);

exception.Message.ShouldBe("Test failed: Test failed | Multiple hooks failed: Hook 1 failed; Hook 2 failed");
exception.TestException.ShouldBe(testException);
exception.HookExceptions.Count.ShouldBe(2);
exception.EventReceiverExceptions.ShouldBeEmpty();
exception.InnerException.ShouldBeOfType<AggregateException>();
var aggEx = (AggregateException)exception.InnerException!;
aggEx.InnerExceptions.Count.ShouldBe(3);
}

[Test]
public void TestExecutionException_WithTestAndEventReceiverExceptions_BuildsCorrectMessage()
{
var testException = new InvalidOperationException("Test failed");
var receiverException = new ArgumentException("Receiver failed");

var exception = new TestExecutionException(testException, [], [receiverException]);

exception.Message.ShouldBe("Test failed: Test failed | Test end event receiver failed: Receiver failed");
exception.TestException.ShouldBe(testException);
exception.HookExceptions.ShouldBeEmpty();
exception.EventReceiverExceptions.Count.ShouldBe(1);
exception.InnerException.ShouldBeOfType<AggregateException>();
var aggEx = (AggregateException)exception.InnerException!;
aggEx.InnerExceptions.Count.ShouldBe(2);
}

[Test]
public void TestExecutionException_WithAllExceptionTypes_BuildsCorrectMessage()
{
var testException = new InvalidOperationException("Test failed");
var hookException1 = new ArgumentException("Hook 1 failed");
var hookException2 = new NullReferenceException("Hook 2 failed");
var receiverException1 = new InvalidCastException("Receiver 1 failed");
var receiverException2 = new System.TimeoutException("Receiver 2 failed");

var exception = new TestExecutionException(
testException,
[hookException1, hookException2],
[receiverException1, receiverException2]);

exception.Message.ShouldBe(
"Test failed: Test failed | " +
"Multiple hooks failed: Hook 1 failed; Hook 2 failed | " +
"2 test end event receivers failed: Receiver 1 failed; Receiver 2 failed");

exception.TestException.ShouldBe(testException);
exception.HookExceptions.Count.ShouldBe(2);
exception.EventReceiverExceptions.Count.ShouldBe(2);
exception.InnerException.ShouldBeOfType<AggregateException>();
var aggEx = (AggregateException)exception.InnerException!;
aggEx.InnerExceptions.Count.ShouldBe(5);
}

[Test]
public void TestExecutionException_WithOnlyHookAndReceiverExceptions_BuildsCorrectMessage()
{
var hookException = new ArgumentException("Hook failed");
var receiverException = new InvalidOperationException("Receiver failed");

var exception = new TestExecutionException(null, [hookException], [receiverException]);

exception.Message.ShouldBe("Hook failed | Test end event receiver failed: Receiver failed");
exception.TestException.ShouldBeNull();
exception.HookExceptions.Count.ShouldBe(1);
exception.EventReceiverExceptions.Count.ShouldBe(1);
exception.InnerException.ShouldBeOfType<AggregateException>();
var aggEx = (AggregateException)exception.InnerException!;
aggEx.InnerExceptions.Count.ShouldBe(2);
}

[Test]
public void TestExecutionException_WithNoExceptions_BuildsEmptyMessage()
{
var exception = new TestExecutionException(null, [], []);

exception.Message.ShouldBeEmpty();
exception.TestException.ShouldBeNull();
exception.HookExceptions.ShouldBeEmpty();
exception.EventReceiverExceptions.ShouldBeEmpty();
exception.InnerException.ShouldBeNull();
}

[Test]
public void TestExecutionException_PreservesOriginalExceptionStackTrace()
{
var testException = CreateExceptionWithStackTrace();

var exception = new TestExecutionException(testException, [], []);

exception.InnerException.ShouldBe(testException);
exception.InnerException!.StackTrace.ShouldNotBeNull();
}

private static Exception CreateExceptionWithStackTrace()
{
try
{
throw new InvalidOperationException("Test exception with stack trace");
}
catch (Exception ex)
{
return ex;
}
}
}
15 changes: 10 additions & 5 deletions TUnit.Engine/Services/EventReceiverOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,18 +123,20 @@ private async ValueTask InvokeTestStartEventReceiversCore(TestContext context, C
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public async ValueTask InvokeTestEndEventReceiversAsync(TestContext context, CancellationToken cancellationToken)
public async ValueTask<List<Exception>> InvokeTestEndEventReceiversAsync(TestContext context, CancellationToken cancellationToken)
{
if (!_registry.HasTestEndReceivers())
{
return;
return [];
}

await InvokeTestEndEventReceiversCore(context, cancellationToken);
return await InvokeTestEndEventReceiversCore(context, cancellationToken);
}

private async ValueTask InvokeTestEndEventReceiversCore(TestContext context, CancellationToken cancellationToken)
private async ValueTask<List<Exception>> InvokeTestEndEventReceiversCore(TestContext context, CancellationToken cancellationToken)
{
var exceptions = new List<Exception>();

// Manual filtering and sorting instead of LINQ to avoid allocations
var eligibleObjects = context.GetEligibleEventObjects();
List<ITestEndEventReceiver>? receivers = null;
Expand All @@ -150,7 +152,7 @@ private async ValueTask InvokeTestEndEventReceiversCore(TestContext context, Can

if (receivers == null)
{
return;
return exceptions;
}

// Manual sort instead of OrderBy
Expand All @@ -167,8 +169,11 @@ private async ValueTask InvokeTestEndEventReceiversCore(TestContext context, Can
catch (Exception ex)
{
await _logger.LogErrorAsync($"Error in test end event receiver: {ex.Message}");
exceptions.Add(ex);
}
}

return exceptions;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
Loading
Loading