diff --git a/TUnit.Engine/Services/EventReceiverOrchestrator.cs b/TUnit.Engine/Services/EventReceiverOrchestrator.cs index 0d127feb35..a9777470a8 100644 --- a/TUnit.Engine/Services/EventReceiverOrchestrator.cs +++ b/TUnit.Engine/Services/EventReceiverOrchestrator.cs @@ -40,43 +40,37 @@ public EventReceiverOrchestrator(TUnitFrameworkLogger logger) public void RegisterReceivers(TestContext context, CancellationToken cancellationToken) { - var objectsToRegister = new List(); + List? 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); } @@ -147,7 +141,8 @@ public async ValueTask> InvokeTestEndEventReceiversAsync(TestCon private async ValueTask> InvokeTestEndEventReceiversCore(TestContext context, CancellationToken cancellationToken, EventReceiverStage? stage) { - var exceptions = new List(); + // Defer exception list allocation until actually needed + List? exceptions = null; // Manual filtering and sorting instead of LINQ to avoid allocations var eligibleObjects = context.GetEligibleEventObjects(); @@ -171,7 +166,7 @@ private async ValueTask> InvokeTestEndEventReceiversCore(TestCon if (receivers == null) { - return exceptions; + return []; } // Manual sort instead of OrderBy @@ -188,11 +183,12 @@ private async ValueTask> 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)] diff --git a/TUnit.Engine/Services/HookExecutor.cs b/TUnit.Engine/Services/HookExecutor.cs index 5421310afd..e971037544 100644 --- a/TUnit.Engine/Services/HookExecutor.cs +++ b/TUnit.Engine/Services/HookExecutor.cs @@ -64,14 +64,16 @@ public async ValueTask ExecuteBeforeTestSessionHooksAsync(CancellationToken canc public async ValueTask> ExecuteAfterTestSessionHooksAsync(CancellationToken cancellationToken) { - var exceptions = new List(); var hooks = await _hookCollectionService.CollectAfterTestSessionHooksAsync().ConfigureAwait(false); if (hooks.Count == 0) { - return exceptions; + return []; } + // Defer exception list allocation until actually needed + List? exceptions = null; + foreach (var hook in hooks) { try @@ -83,11 +85,12 @@ public async ValueTask> 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) @@ -126,14 +129,16 @@ public async ValueTask ExecuteBeforeAssemblyHooksAsync(Assembly assembly, Cancel public async ValueTask> ExecuteAfterAssemblyHooksAsync(Assembly assembly, CancellationToken cancellationToken) { - var exceptions = new List(); var hooks = await _hookCollectionService.CollectAfterAssemblyHooksAsync(assembly).ConfigureAwait(false); if (hooks.Count == 0) { - return exceptions; + return []; } + // Defer exception list allocation until actually needed + List? exceptions = null; + foreach (var hook in hooks) { try @@ -146,11 +151,12 @@ public async ValueTask> 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( @@ -193,14 +199,16 @@ public async ValueTask> ExecuteAfterClassHooksAsync( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)] Type testClass, CancellationToken cancellationToken) { - var exceptions = new List(); var hooks = await _hookCollectionService.CollectAfterClassHooksAsync(testClass).ConfigureAwait(false); if (hooks.Count == 0) { - return exceptions; + return []; } + // Defer exception list allocation until actually needed + List? exceptions = null; + foreach (var hook in hooks) { try @@ -213,11 +221,12 @@ public async ValueTask> 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) @@ -285,7 +294,8 @@ public async ValueTask ExecuteBeforeTestHooksAsync(AbstractExecutableTest test, public async ValueTask> ExecuteAfterTestHooksAsync(AbstractExecutableTest test, CancellationToken cancellationToken) { - var exceptions = new List(); + // Defer exception list allocation until actually needed + List? exceptions = null; var testClassType = test.Metadata.TestClassType; // Execute After(Test) hooks first (specific hooks run before global hooks for cleanup) @@ -302,6 +312,7 @@ public async ValueTask> ExecuteAfterTestHooksAsync(AbstractExecu } catch (Exception ex) { + exceptions ??= []; exceptions.Add(new AfterTestException($"After(Test) hook failed: {ex.Message}", ex)); } } @@ -321,12 +332,13 @@ public async ValueTask> 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)