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
38 changes: 37 additions & 1 deletion TUnit.Engine/Services/EventReceiverOrchestrator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using TUnit.Core;
Expand Down Expand Up @@ -42,7 +43,7 @@ public EventReceiverOrchestrator(TUnitFrameworkLogger logger)
_logger = logger;
}

public void RegisterReceivers(TestContext context, CancellationToken cancellationToken)
public void RegisterReceivers(TestContext context)
{
var vlb = new ValueListBuilder<object>([null, null, null, null]);

Expand Down Expand Up @@ -80,6 +81,41 @@ obj is IFirstTestInAssemblyEventReceiver ||
vlb.Dispose();
}

// Precondition: ClassInstance has just been assigned; the initial RegisterReceivers call (with ClassInstance still null) already covered every other eligible object.
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)
{
return;
}

// Defense-in-depth: SkippedTestInstance is a sentinel singleton for tests skipped
// at registration time and should never be treated as an event receiver. Callers
// already short-circuit on this sentinel, but guard here too.
if (classInstance is SkippedTestInstance)
{
return;
}

if (!_initializedObjects.Add(classInstance))
{
return;
}

if (classInstance is IFirstTestInTestSessionEventReceiver
or IFirstTestInAssemblyEventReceiver
or IFirstTestInClassEventReceiver)
{
if (!_registeredFirstEventReceiverTypes.Add(classInstance.GetType()))
{
return;
}
}

_registry.RegisterReceiver(classInstance);
}

// Fast-path checks with inlining
[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
12 changes: 7 additions & 5 deletions TUnit.Engine/Services/TestExecution/TestCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,16 @@ private async ValueTask ExecuteTestInternalAsync(AbstractExecutableTest test, Ca
try
{
_stateManager.MarkRunning(test);
// Fire-and-forget InProgress - it's informational and doesn't need to block test execution
_ = _messageBus.InProgress(test.Context);
// Await InProgress so back-pressure from MTP's bounded channel spreads publishes
// across each test task rather than fanning 1000+ fire-and-forget writers into
// the channel at once.
await _messageBus.InProgress(test.Context).ConfigureAwait(false);

_contextRestorer.RestoreContext(test);

// Register event receivers early so that skip event receivers work
// even when the test is skipped before full initialization.
_eventReceiverOrchestrator.RegisterReceivers(test.Context, cancellationToken);
_eventReceiverOrchestrator.RegisterReceivers(test.Context);

// Check if test was already marked as skipped during registration
// (e.g., by a derived SkipAttribute evaluated in OnTestRegistered).
Expand Down Expand Up @@ -290,7 +292,7 @@ private async ValueTask ExecuteTestLifecycleAsync(AbstractExecutableTest test, C

test.Context.Metadata.TestDetails.ClassInstance = await test.CreateInstanceAsync().ConfigureAwait(false);

// Invalidate cached eligible event objects since ClassInstance changed
// Drop the cached eligible-objects list so any later consumer rebuilds it with the new ClassInstance included — the initial list was built before the instance existed.
test.Context.CachedEligibleEventObjects = null;

// Check if this test should be skipped (after creating instance).
Expand All @@ -310,7 +312,7 @@ private async ValueTask ExecuteTestLifecycleAsync(AbstractExecutableTest test, C

try
{
_testInitializer.PrepareTest(test, cancellationToken);
_testInitializer.PrepareTest(test);
test.Context.RestoreExecutionContext();
var testTimeout = test.Context.Metadata.TestDetails.Timeout;
await _testExecutor.ExecuteAsync(test, _testInitializer, cancellationToken, testTimeout).ConfigureAwait(false);
Expand Down
12 changes: 7 additions & 5 deletions TUnit.Engine/TestInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ public TestInitializer(
_objectLifecycleService = objectLifecycleService;
}

public void PrepareTest(AbstractExecutableTest test, CancellationToken cancellationToken)
public void PrepareTest(AbstractExecutableTest test)
{
// Register event receivers
_eventReceiverOrchestrator.RegisterReceivers(test.Context, cancellationToken);
// Register the freshly created ClassInstance as an event receiver. The initial
// registration happens before instance creation, so re-iterating the full
// eligible-event-object set would duplicate work — only the ClassInstance is new.
_eventReceiverOrchestrator.RegisterClassInstanceReceiver(test.Context);

// Prepare test: set cached property values on the instance
// Does NOT call IAsyncInitializer - that is deferred until after BeforeClass hooks
// Prepare test: set cached property values on the instance.
// Does NOT call IAsyncInitializer - that is deferred until after BeforeClass hooks.
_objectLifecycleService.PrepareTest(test.Context);
}

Expand Down
Loading