From 57f4d2857ed9b718e03896b7fa335fb10115e27b Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 12 Sep 2026 10:13:04 +0100 Subject: [PATCH] perf: skip receiver registration for ordinary objects --- .../Services/EventReceiverOrchestrator.cs | 9 +- .../EventReceiverRegistrationTests.cs | 84 +++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests/TUnit.UnitTests/EventReceiverRegistrationTests.cs diff --git a/src/TUnit.Engine/Services/EventReceiverOrchestrator.cs b/src/TUnit.Engine/Services/EventReceiverOrchestrator.cs index 47585b0acb..c31ae6eeae 100644 --- a/src/TUnit.Engine/Services/EventReceiverOrchestrator.cs +++ b/src/TUnit.Engine/Services/EventReceiverOrchestrator.cs @@ -49,6 +49,13 @@ public void RegisterReceivers(TestContext context) foreach (var obj in context.GetEligibleEventObjects()) { + // Ordinary attributes, arguments and test instances cannot receive events. + // Avoid retaining them in the deduplication set or scanning every receiver interface. + if (obj is not IEventReceiver) + { + continue; + } + // Use single TryAdd operation instead of Contains + Add if (!_initializedObjects.Add(obj)) { @@ -88,7 +95,7 @@ public void RegisterClassInstanceReceiver(TestContext context) { var classInstance = context.Metadata.TestDetails.ClassInstance; Debug.Assert(classInstance is not null, "RegisterClassInstanceReceiver should only be called after ClassInstance is assigned."); - if (classInstance is null) + if (classInstance is not IEventReceiver) { return; } diff --git a/tests/TUnit.UnitTests/EventReceiverRegistrationTests.cs b/tests/TUnit.UnitTests/EventReceiverRegistrationTests.cs new file mode 100644 index 0000000000..a7c3c81b39 --- /dev/null +++ b/tests/TUnit.UnitTests/EventReceiverRegistrationTests.cs @@ -0,0 +1,84 @@ +using TUnit.Core.Interfaces; +using TUnit.Engine.Services; + +namespace TUnit.UnitTests; + +public class EventReceiverRegistrationTests +{ + [Test] + public void OrdinaryObjectsDoNotParticipateInReceiverDeduplication() + { + var context = CreateContext(new OrdinaryObject(), [new OrdinaryAttribute()]); + try + { + var orchestrator = new EventReceiverOrchestrator(null!); + orchestrator.RegisterReceivers(context); + orchestrator.RegisterClassInstanceReceiver(context); + } + finally + { + context.RemoveFromRegistry(); + context.Dispose(); + } + } + + [Test] + public async Task AttributeAndClassReceiversStillReceiveEvents() + { + var attribute = new ReceiverAttribute(); + var instance = new ReceiverAttribute(); + var context = CreateContext(null!, [attribute]); + try + { + var orchestrator = new EventReceiverOrchestrator(null!); + orchestrator.RegisterReceivers(context); + context.Metadata.TestDetails.ClassInstance = instance; + orchestrator.RegisterClassInstanceReceiver(context); + orchestrator.RegisterClassInstanceReceiver(context); + + await orchestrator.InvokeTestStartEventReceiversAsync(context, CancellationToken.None); + await Assert.That(attribute.Calls).IsEqualTo(1); + await Assert.That(instance.Calls).IsEqualTo(1); + } + finally + { + context.RemoveFromRegistry(); + context.Dispose(); + } + } + + private static TestContext CreateContext(object instance, Attribute[] attributes) + { + var current = TestContext.Current!; + var context = new TestContext("Registration", current.ServiceProvider, current.ClassContext, + new TestBuilderContext { TestMetadata = current.TestDetails.MethodMetadata }, CancellationToken.None); + context.Metadata.TestDetails = new TestDetails(attributes) + { + TestId = context.Id, TestName = "Registration", ClassType = typeof(EventReceiverRegistrationTests), + MethodName = "Registration", ClassInstance = instance, TestMethodArguments = [], TestClassArguments = [], + MethodMetadata = current.TestDetails.MethodMetadata, ReturnType = typeof(void), + AttributesByType = new Dictionary>() + }; + return context; + } + + private sealed class OrdinaryObject + { + public override int GetHashCode() => throw new InvalidOperationException("Not an event receiver"); + } + + private sealed class OrdinaryAttribute : Attribute + { + public override int GetHashCode() => throw new InvalidOperationException("Not an event receiver"); + } + + private sealed class ReceiverAttribute : Attribute, ITestStartEventReceiver + { + public int Calls { get; private set; } + public ValueTask OnTestStart(TestContext context) + { + Calls++; + return ValueTask.CompletedTask; + } + } +}