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
48 changes: 22 additions & 26 deletions TUnit.Engine/Services/EventReceiverOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,43 +40,37 @@ public EventReceiverOrchestrator(TUnitFrameworkLogger logger)

public void RegisterReceivers(TestContext context, CancellationToken cancellationToken)
{
var objectsToRegister = new List<object>();
List<object>? objectsToRegister = null;

foreach (var obj in context.GetEligibleEventObjects())
{
if (_initializedObjects.Contains(obj))
// Use single TryAdd operation instead of Contains + Add
if (!_initializedObjects.Add(obj))
{
continue;
}

if (_initializedObjects.Add(obj))
{
bool isFirstEventReceiver = obj is IFirstTestInTestSessionEventReceiver ||
obj is IFirstTestInAssemblyEventReceiver ||
obj is IFirstTestInClassEventReceiver;

if (isFirstEventReceiver)
{
var objType = obj.GetType();
bool isFirstEventReceiver = obj is IFirstTestInTestSessionEventReceiver ||
obj is IFirstTestInAssemblyEventReceiver ||
obj is IFirstTestInClassEventReceiver;

if (_registeredFirstEventReceiverTypes.Contains(objType))
{
continue;
}
if (isFirstEventReceiver)
{
var objType = obj.GetType();

if (_registeredFirstEventReceiverTypes.Add(objType))
{
objectsToRegister.Add(obj);
}
}
else
// Use single TryAdd operation instead of Contains + Add
if (!_registeredFirstEventReceiverTypes.Add(objType))
{
objectsToRegister.Add(obj);
continue;
}
}

// Defer list allocation until actually needed
objectsToRegister ??= [];
objectsToRegister.Add(obj);
}

if (objectsToRegister.Count > 0)
if (objectsToRegister is { Count: > 0 })
{
_registry.RegisterReceivers(objectsToRegister);
}
Expand Down Expand Up @@ -147,7 +141,8 @@ public async ValueTask<List<Exception>> InvokeTestEndEventReceiversAsync(TestCon

private async ValueTask<List<Exception>> InvokeTestEndEventReceiversCore(TestContext context, CancellationToken cancellationToken, EventReceiverStage? stage)
{
var exceptions = new List<Exception>();
// Defer exception list allocation until actually needed
List<Exception>? exceptions = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to move this down after receivers == null check as well. This was done everywhere else


// Manual filtering and sorting instead of LINQ to avoid allocations
var eligibleObjects = context.GetEligibleEventObjects();
Expand All @@ -171,7 +166,7 @@ private async ValueTask<List<Exception>> InvokeTestEndEventReceiversCore(TestCon

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

// Manual sort instead of OrderBy
Expand All @@ -188,11 +183,12 @@ private async ValueTask<List<Exception>> InvokeTestEndEventReceiversCore(TestCon
catch (Exception ex)
{
await _logger.LogErrorAsync($"Error in test end event receiver: {ex.Message}");
exceptions ??= [];
exceptions.Add(ex);
}
}

return exceptions;
return exceptions ?? [];
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
34 changes: 23 additions & 11 deletions TUnit.Engine/Services/HookExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,16 @@ public async ValueTask ExecuteBeforeTestSessionHooksAsync(CancellationToken canc

public async ValueTask<List<Exception>> ExecuteAfterTestSessionHooksAsync(CancellationToken cancellationToken)
{
var exceptions = new List<Exception>();
var hooks = await _hookCollectionService.CollectAfterTestSessionHooksAsync().ConfigureAwait(false);

if (hooks.Count == 0)
{
return exceptions;
return [];
}

// Defer exception list allocation until actually needed
List<Exception>? exceptions = null;

foreach (var hook in hooks)
{
try
Expand All @@ -83,11 +85,12 @@ public async ValueTask<List<Exception>> ExecuteAfterTestSessionHooksAsync(Cancel
{
// Collect hook exceptions instead of throwing immediately
// This allows all hooks to run even if some fail
exceptions ??= [];
exceptions.Add(new AfterTestSessionException($"AfterTestSession hook failed: {ex.Message}", ex));
}
}

return exceptions;
return exceptions ?? [];
}

public async ValueTask ExecuteBeforeAssemblyHooksAsync(Assembly assembly, CancellationToken cancellationToken)
Expand Down Expand Up @@ -126,14 +129,16 @@ public async ValueTask ExecuteBeforeAssemblyHooksAsync(Assembly assembly, Cancel

public async ValueTask<List<Exception>> ExecuteAfterAssemblyHooksAsync(Assembly assembly, CancellationToken cancellationToken)
{
var exceptions = new List<Exception>();
var hooks = await _hookCollectionService.CollectAfterAssemblyHooksAsync(assembly).ConfigureAwait(false);

if (hooks.Count == 0)
{
return exceptions;
return [];
}

// Defer exception list allocation until actually needed
List<Exception>? exceptions = null;

foreach (var hook in hooks)
{
try
Expand All @@ -146,11 +151,12 @@ public async ValueTask<List<Exception>> ExecuteAfterAssemblyHooksAsync(Assembly
{
// Collect hook exceptions instead of throwing immediately
// This allows all hooks to run even if some fail
exceptions ??= [];
exceptions.Add(new AfterAssemblyException($"AfterAssembly hook failed: {ex.Message}", ex));
}
}

return exceptions;
return exceptions ?? [];
}

public async ValueTask ExecuteBeforeClassHooksAsync(
Expand Down Expand Up @@ -193,14 +199,16 @@ public async ValueTask<List<Exception>> ExecuteAfterClassHooksAsync(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
Type testClass, CancellationToken cancellationToken)
{
var exceptions = new List<Exception>();
var hooks = await _hookCollectionService.CollectAfterClassHooksAsync(testClass).ConfigureAwait(false);

if (hooks.Count == 0)
{
return exceptions;
return [];
}

// Defer exception list allocation until actually needed
List<Exception>? exceptions = null;

foreach (var hook in hooks)
{
try
Expand All @@ -213,11 +221,12 @@ public async ValueTask<List<Exception>> ExecuteAfterClassHooksAsync(
{
// Collect hook exceptions instead of throwing immediately
// This allows all hooks to run even if some fail
exceptions ??= [];
exceptions.Add(new AfterClassException($"AfterClass hook failed: {ex.Message}", ex));
}
}

return exceptions;
return exceptions ?? [];
}

public async ValueTask ExecuteBeforeTestHooksAsync(AbstractExecutableTest test, CancellationToken cancellationToken)
Expand Down Expand Up @@ -285,7 +294,8 @@ public async ValueTask ExecuteBeforeTestHooksAsync(AbstractExecutableTest test,

public async ValueTask<List<Exception>> ExecuteAfterTestHooksAsync(AbstractExecutableTest test, CancellationToken cancellationToken)
{
var exceptions = new List<Exception>();
// Defer exception list allocation until actually needed
List<Exception>? exceptions = null;
var testClassType = test.Metadata.TestClassType;

// Execute After(Test) hooks first (specific hooks run before global hooks for cleanup)
Expand All @@ -302,6 +312,7 @@ public async ValueTask<List<Exception>> ExecuteAfterTestHooksAsync(AbstractExecu
}
catch (Exception ex)
{
exceptions ??= [];
exceptions.Add(new AfterTestException($"After(Test) hook failed: {ex.Message}", ex));
}
}
Expand All @@ -321,12 +332,13 @@ public async ValueTask<List<Exception>> ExecuteAfterTestHooksAsync(AbstractExecu
}
catch (Exception ex)
{
exceptions ??= [];
exceptions.Add(new AfterTestException($"AfterEvery(Test) hook failed: {ex.Message}", ex));
}
}
}

return exceptions;
return exceptions ?? [];
}

public async ValueTask ExecuteBeforeTestDiscoveryHooksAsync(CancellationToken cancellationToken)
Expand Down
Loading