diff --git a/TUnit.Engine/Building/TestBuilder.cs b/TUnit.Engine/Building/TestBuilder.cs index 287a78c001..74f05c6bf5 100644 --- a/TUnit.Engine/Building/TestBuilder.cs +++ b/TUnit.Engine/Building/TestBuilder.cs @@ -1038,15 +1038,13 @@ private async Task InvokeTestRegisteredEventReceiversAsync(TestContext context) #if NET6_0_OR_GREATER [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Scoped attribute filtering uses Type.GetInterfaces and reflection")] #endif - private async Task InvokeDiscoveryEventReceiversAsync(TestContext context) + private Task InvokeDiscoveryEventReceiversAsync(TestContext context) { var discoveredContext = new DiscoveredTestContext( context.Metadata.TestDetails.TestName, context); - { - await _eventReceiverOrchestrator.InvokeTestDiscoveryEventReceiversAsync(context, discoveredContext, CancellationToken.None); - } + return _eventReceiverOrchestrator.InvokeTestDiscoveryEventReceiversAsync(context, discoveredContext, CancellationToken.None); } private async Task CreateFailedTestForDataGenerationError(TestMetadata metadata, Exception exception) diff --git a/TUnit.Engine/Events/EventReceiverRegistry.cs b/TUnit.Engine/Events/EventReceiverRegistry.cs index d7bfd127db..ca0c90266c 100644 --- a/TUnit.Engine/Events/EventReceiverRegistry.cs +++ b/TUnit.Engine/Events/EventReceiverRegistry.cs @@ -23,16 +23,16 @@ private enum EventTypes LastTestInClass = 1 << 9, All = ~0 } - + private volatile EventTypes _registeredEvents = EventTypes.None; private readonly Dictionary _receiversByType = new(); private readonly Dictionary _cachedTypedReceivers = new(); private readonly ReaderWriterLockSlim _lock = new(); - + /// /// Register event receivers from a collection of objects /// - public void RegisterReceivers(IEnumerable objects) + public void RegisterReceivers(ReadOnlySpan objects) { _lock.EnterWriteLock(); try @@ -47,7 +47,7 @@ public void RegisterReceivers(IEnumerable objects) _lock.ExitWriteLock(); } } - + /// /// Register a single event receiver /// @@ -63,7 +63,7 @@ public void RegisterReceiver(object receiver) _lock.ExitWriteLock(); } } - + private void RegisterReceiverInternal(object receiver) { UpdateEventFlags(receiver); @@ -83,7 +83,7 @@ private void RegisterReceiverInternal(object receiver) _cachedTypedReceivers.Clear(); } - + private void RegisterIfImplements(object receiver) where T : class { if (receiver is T) @@ -102,43 +102,43 @@ private void RegisterIfImplements(object receiver) where T : class } } } - + /// /// Fast check if any receivers registered for event type /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasTestStartReceivers() => (_registeredEvents & EventTypes.TestStart) != 0; - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasTestEndReceivers() => (_registeredEvents & EventTypes.TestEnd) != 0; - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasTestSkippedReceivers() => (_registeredEvents & EventTypes.TestSkipped) != 0; - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasTestRegisteredReceivers() => (_registeredEvents & EventTypes.TestRegistered) != 0; - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasFirstTestInSessionReceivers() => (_registeredEvents & EventTypes.FirstTestInSession) != 0; - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasLastTestInSessionReceivers() => (_registeredEvents & EventTypes.LastTestInSession) != 0; - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasFirstTestInAssemblyReceivers() => (_registeredEvents & EventTypes.FirstTestInAssembly) != 0; - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasLastTestInAssemblyReceivers() => (_registeredEvents & EventTypes.LastTestInAssembly) != 0; - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasFirstTestInClassReceivers() => (_registeredEvents & EventTypes.FirstTestInClass) != 0; - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasLastTestInClassReceivers() => (_registeredEvents & EventTypes.LastTestInClass) != 0; - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasAnyReceivers() => _registeredEvents != EventTypes.None; - + public T[] GetReceiversOfType() where T : class { var typeKey = typeof(T); @@ -202,7 +202,7 @@ public T[] GetReceiversOfType() where T : class _lock.ExitUpgradeableReadLock(); } } - + private void UpdateEventFlags(object receiver) { if (receiver is ITestStartEventReceiver) @@ -246,10 +246,10 @@ private void UpdateEventFlags(object receiver) _registeredEvents |= EventTypes.LastTestInClass; } } - - + + public void Dispose() { _lock?.Dispose(); } -} \ No newline at end of file +} diff --git a/TUnit.Engine/Services/EventReceiverOrchestrator.cs b/TUnit.Engine/Services/EventReceiverOrchestrator.cs index 216d2169fd..8a28dee848 100644 --- a/TUnit.Engine/Services/EventReceiverOrchestrator.cs +++ b/TUnit.Engine/Services/EventReceiverOrchestrator.cs @@ -40,7 +40,7 @@ public EventReceiverOrchestrator(TUnitFrameworkLogger logger) public void RegisterReceivers(TestContext context, CancellationToken cancellationToken) { - List? objectsToRegister = null; + var vlb = new ValueListBuilder([null, null, null, null]); foreach (var obj in context.GetEligibleEventObjects()) { @@ -66,14 +66,14 @@ obj is IFirstTestInAssemblyEventReceiver || } // Defer list allocation until actually needed - objectsToRegister ??= []; - objectsToRegister.Add(obj); + vlb.Append(obj); } - if (objectsToRegister is { Count: > 0 }) + if (vlb.Length > 0) { - _registry.RegisterReceivers(objectsToRegister); + _registry.RegisterReceivers(vlb.AsSpan()); } + vlb.Dispose(); } @@ -133,6 +133,26 @@ public ValueTask> InvokeTestEndEventReceiversAsync(Test { return new ValueTask>([]); } +#if NET + if (stage.HasValue) + { + var receivers = context.GetTestEndReceivers(stage.Value); + if (receivers.Length == 0) + { + return new ValueTask>([]); + } + } + else + { + var earlyReceivers = context.GetTestEndReceivers(EventReceiverStage.Early); + var lateReceivers = context.GetTestEndReceivers(EventReceiverStage.Late); + + if (earlyReceivers.Length == 0 && lateReceivers.Length == 0) + { + return new ValueTask>([]); + } + } +#endif return InvokeTestEndEventReceiversCore(context, cancellationToken, stage); } @@ -241,11 +261,21 @@ private async ValueTask InvokeTestSkippedEventReceiversCore(TestContext context, } } - public async ValueTask InvokeTestDiscoveryEventReceiversAsync(TestContext context, DiscoveredTestContext discoveredContext, CancellationToken cancellationToken) + public Task InvokeTestDiscoveryEventReceiversAsync(TestContext context, DiscoveredTestContext discoveredContext, CancellationToken cancellationToken) { // Use pre-computed receivers (already filtered, sorted, and scoped-attribute filtered) var receivers = context.GetTestDiscoveryReceivers(); + if(receivers.Length == 0) + { + return Task.CompletedTask; + } + + return InvokeTestDiscoveryEventReceiversCoreAsync(receivers, discoveredContext); + } + + private static async Task InvokeTestDiscoveryEventReceiversCoreAsync(ITestDiscoveryEventReceiver[] receivers, DiscoveredTestContext discoveredContext) + { foreach (var receiver in receivers) { await receiver.OnTestDiscovered(discoveredContext);