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

/// <summary>
/// Defines the execution stage for event receivers relative to instance-level hooks
/// </summary>
public enum EventReceiverStage
{
/// <summary>
/// Execute before instance-level hooks ([Before(Test)], [After(Test)])
/// </summary>
Early = 0,

/// <summary>
/// Execute after instance-level hooks (default behavior for backward compatibility)
/// </summary>
Late = 1
}
18 changes: 18 additions & 0 deletions TUnit.Core/Interfaces/ITestEndEventReceiver.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
namespace TUnit.Core.Interfaces;

using TUnit.Core.Enums;

/// <summary>
/// Simplified interface for test end event receivers
/// </summary>
Expand All @@ -9,4 +11,20 @@ public interface ITestEndEventReceiver : IEventReceiver
/// Called when a test ends
/// </summary>
ValueTask OnTestEnd(TestContext context);

/// <summary>
/// Gets the execution stage of this event receiver relative to instance-level hooks.
/// </summary>
/// <remarks>
/// Early stage executes before [After(Test)] hooks, Late stage executes after.
/// Default is Late for backward compatibility.
/// This property is only available on .NET 8.0+ due to default interface member requirements.
/// On older frameworks, all receivers execute at Late stage.
/// </remarks>
/// <value>
/// The execution stage. Default is <see cref="EventReceiverStage.Late"/>.
/// </value>
#if NET
public EventReceiverStage Stage => EventReceiverStage.Late;
#endif
}
18 changes: 18 additions & 0 deletions TUnit.Core/Interfaces/ITestStartEventReceiver.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
namespace TUnit.Core.Interfaces;

using TUnit.Core.Enums;

/// <summary>
/// Simplified interface for test start event receivers
/// </summary>
Expand All @@ -9,4 +11,20 @@ public interface ITestStartEventReceiver : IEventReceiver
/// Called when a test starts
/// </summary>
ValueTask OnTestStart(TestContext context);

/// <summary>
/// Gets the execution stage of this event receiver relative to instance-level hooks.
/// </summary>
/// <remarks>
/// Early stage executes before [Before(Test)] hooks, Late stage executes after.
/// Default is Late for backward compatibility.
/// This property is only available on .NET 8.0+ due to default interface member requirements.
/// On older frameworks, all receivers execute at Late stage.
/// </remarks>
/// <value>
/// The execution stage. Default is <see cref="EventReceiverStage.Late"/>.
/// </value>
#if NET
public EventReceiverStage Stage => EventReceiverStage.Late;
#endif
}
27 changes: 21 additions & 6 deletions TUnit.Engine/Services/EventReceiverOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Runtime.CompilerServices;
using TUnit.Core;
using TUnit.Core.Data;
using TUnit.Core.Enums;
using TUnit.Core.Helpers;
using TUnit.Core.Interfaces;
using TUnit.Core.Tracking;
Expand Down Expand Up @@ -87,18 +88,18 @@ obj is IFirstTestInAssemblyEventReceiver ||

// Fast-path checks with inlining
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public async ValueTask InvokeTestStartEventReceiversAsync(TestContext context, CancellationToken cancellationToken)
public async ValueTask InvokeTestStartEventReceiversAsync(TestContext context, CancellationToken cancellationToken, EventReceiverStage? stage = null)
{
// Fast path - no allocation if no receivers
if (!_registry.HasTestStartReceivers())
{
return;
}

await InvokeTestStartEventReceiversCore(context, cancellationToken);
await InvokeTestStartEventReceiversCore(context, cancellationToken, stage);
}

private async ValueTask InvokeTestStartEventReceiversCore(TestContext context, CancellationToken cancellationToken)
private async ValueTask InvokeTestStartEventReceiversCore(TestContext context, CancellationToken cancellationToken, EventReceiverStage? stage)
{
// Manual filtering and sorting instead of LINQ to avoid allocations
var eligibleObjects = context.GetEligibleEventObjects();
Expand All @@ -108,6 +109,13 @@ private async ValueTask InvokeTestStartEventReceiversCore(TestContext context, C
{
if (obj is ITestStartEventReceiver receiver)
{
#if NET
// Filter by stage if specified (only on .NET 8.0+ where Stage property exists)
if (stage.HasValue && receiver.Stage != stage.Value)
{
continue;
}
#endif
receivers ??= [];
receivers.Add(receiver);
}
Expand All @@ -130,17 +138,17 @@ private async ValueTask InvokeTestStartEventReceiversCore(TestContext context, C
}

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

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

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

Expand All @@ -152,6 +160,13 @@ private async ValueTask<List<Exception>> InvokeTestEndEventReceiversCore(TestCon
{
if (obj is ITestEndEventReceiver receiver)
{
#if NET
// Filter by stage if specified (only on .NET 8.0+ where Stage property exists)
if (stage.HasValue && receiver.Stage != stage.Value)
{
continue;
}
#endif
receivers ??= [];
receivers.Add(receiver);
}
Expand Down
21 changes: 19 additions & 2 deletions TUnit.Engine/TestExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Reflection;
using System.Runtime.ExceptionServices;
using TUnit.Core;
using TUnit.Core.Enums;
using TUnit.Core.Exceptions;
using TUnit.Core.Interfaces;
using TUnit.Core.Services;
Expand Down Expand Up @@ -91,9 +92,15 @@ await _eventReceiverOrchestrator.InvokeFirstTestInClassEventReceiversAsync(

executableTest.Context.ClassContext.RestoreExecutionContext();

// Early stage test start receivers run before instance-level hooks
await _eventReceiverOrchestrator.InvokeTestStartEventReceiversAsync(executableTest.Context, cancellationToken, EventReceiverStage.Early).ConfigureAwait(false);

executableTest.Context.RestoreExecutionContext();

await _hookExecutor.ExecuteBeforeTestHooksAsync(executableTest, cancellationToken).ConfigureAwait(false);

await _eventReceiverOrchestrator.InvokeTestStartEventReceiversAsync(executableTest.Context, cancellationToken).ConfigureAwait(false);
// Late stage test start receivers run after instance-level hooks (default behavior)
await _eventReceiverOrchestrator.InvokeTestStartEventReceiversAsync(executableTest.Context, cancellationToken, EventReceiverStage.Late).ConfigureAwait(false);

executableTest.Context.RestoreExecutionContext();

Expand All @@ -114,8 +121,18 @@ await _eventReceiverOrchestrator.InvokeFirstTestInClassEventReceiversAsync(
}
finally
{
// Early stage test end receivers run before instance-level hooks
var earlyStageExceptions = await _eventReceiverOrchestrator.InvokeTestEndEventReceiversAsync(executableTest.Context, cancellationToken, EventReceiverStage.Early).ConfigureAwait(false);

var hookExceptions = await _hookExecutor.ExecuteAfterTestHooksAsync(executableTest, cancellationToken).ConfigureAwait(false);
var eventReceiverExceptions = await _eventReceiverOrchestrator.InvokeTestEndEventReceiversAsync(executableTest.Context, cancellationToken).ConfigureAwait(false);

// Late stage test end receivers run after instance-level hooks (default behavior)
var lateStageExceptions = await _eventReceiverOrchestrator.InvokeTestEndEventReceiversAsync(executableTest.Context, cancellationToken, EventReceiverStage.Late).ConfigureAwait(false);

// Combine all exceptions from event receivers
var eventReceiverExceptions = new List<Exception>(earlyStageExceptions.Count + lateStageExceptions.Count);
eventReceiverExceptions.AddRange(earlyStageExceptions);
eventReceiverExceptions.AddRange(lateStageExceptions);

if (hookExceptions.Count > 0 || eventReceiverExceptions.Count > 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1665,6 +1665,11 @@ namespace .Enums
TestParameters = 1,
Property = 2,
}
public enum EventReceiverStage
{
Early = 0,
Late = 1,
}
public enum LogLevel
{
None = -1,
Expand Down Expand Up @@ -2328,6 +2333,7 @@ namespace .Interfaces
}
public interface ITestEndEventReceiver : .
{
. Stage { get; }
. OnTestEnd(.TestContext context);
}
public interface ITestEvents
Expand Down Expand Up @@ -2443,6 +2449,7 @@ namespace .Interfaces
}
public interface ITestStartEventReceiver : .
{
. Stage { get; }
. OnTestStart(.TestContext context);
}
public interface ITestStateBag
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1665,6 +1665,11 @@ namespace .Enums
TestParameters = 1,
Property = 2,
}
public enum EventReceiverStage
{
Early = 0,
Late = 1,
}
public enum LogLevel
{
None = -1,
Expand Down Expand Up @@ -2328,6 +2333,7 @@ namespace .Interfaces
}
public interface ITestEndEventReceiver : .
{
. Stage { get; }
. OnTestEnd(.TestContext context);
}
public interface ITestEvents
Expand Down Expand Up @@ -2443,6 +2449,7 @@ namespace .Interfaces
}
public interface ITestStartEventReceiver : .
{
. Stage { get; }
. OnTestStart(.TestContext context);
}
public interface ITestStateBag
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1665,6 +1665,11 @@ namespace .Enums
TestParameters = 1,
Property = 2,
}
public enum EventReceiverStage
{
Early = 0,
Late = 1,
}
public enum LogLevel
{
None = -1,
Expand Down Expand Up @@ -2328,6 +2333,7 @@ namespace .Interfaces
}
public interface ITestEndEventReceiver : .
{
. Stage { get; }
. OnTestEnd(.TestContext context);
}
public interface ITestEvents
Expand Down Expand Up @@ -2443,6 +2449,7 @@ namespace .Interfaces
}
public interface ITestStartEventReceiver : .
{
. Stage { get; }
. OnTestStart(.TestContext context);
}
public interface ITestStateBag
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1618,6 +1618,11 @@ namespace .Enums
TestParameters = 1,
Property = 2,
}
public enum EventReceiverStage
{
Early = 0,
Late = 1,
}
public enum LogLevel
{
None = -1,
Expand Down
Loading
Loading