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
9 changes: 8 additions & 1 deletion src/TUnit.Engine/Services/EventReceiverOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand Down Expand Up @@ -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;
}
Expand Down
84 changes: 84 additions & 0 deletions tests/TUnit.UnitTests/EventReceiverRegistrationTests.cs
Original file line number Diff line number Diff line change
@@ -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<Type, IReadOnlyList<Attribute>>()
};
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;
}
}
}
Loading