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
2 changes: 1 addition & 1 deletion TUnit.Engine/Interfaces/ITestCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ namespace TUnit.Engine.Interfaces;
/// </summary>
internal interface ITestCoordinator
{
Task ExecuteTestAsync(AbstractExecutableTest test, CancellationToken cancellationToken);
ValueTask ExecuteTestAsync(AbstractExecutableTest test, CancellationToken cancellationToken);
}
6 changes: 3 additions & 3 deletions TUnit.Engine/Scheduling/TestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ internal TestRunner(
private readonly ThreadSafeDictionary<string, Task> _executingTests = new();
private Exception? _firstFailFastException;

public async Task ExecuteTestAsync(AbstractExecutableTest test, CancellationToken cancellationToken)
public async ValueTask ExecuteTestAsync(AbstractExecutableTest test, CancellationToken cancellationToken)
{
// Prevent double execution with a simple lock
var executionTask = _executingTests.GetOrAdd(test.TestId, _ => ExecuteTestInternalAsync(test, cancellationToken));
var executionTask = _executingTests.GetOrAdd(test.TestId, _ => ExecuteTestInternalAsync(test, cancellationToken).AsTask());
await executionTask.ConfigureAwait(false);
}

private async Task ExecuteTestInternalAsync(AbstractExecutableTest test, CancellationToken cancellationToken)
private async ValueTask ExecuteTestInternalAsync(AbstractExecutableTest test, CancellationToken cancellationToken)
{
try
{
Expand Down
10 changes: 3 additions & 7 deletions TUnit.Engine/Scheduling/TestScheduler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ private async Task ExecuteWithGlobalLimitAsync(

try
{
test.ExecutionTask ??= _testRunner.ExecuteTestAsync(test, cancellationToken);
test.ExecutionTask ??= _testRunner.ExecuteTestAsync(test, cancellationToken).AsTask();
await test.ExecutionTask.ConfigureAwait(false);
}
finally
Expand Down Expand Up @@ -458,9 +458,7 @@ await Parallel.ForEachAsync(

try
{
#pragma warning disable IL2026 // ExecuteTestAsync uses reflection, but caller (ExecuteWithGlobalLimitAsync) is already marked with RequiresUnreferencedCode
test.ExecutionTask ??= _testRunner.ExecuteTestAsync(test, ct);
#pragma warning restore IL2026
test.ExecutionTask ??= _testRunner.ExecuteTestAsync(test, ct).AsTask();
await test.ExecutionTask.ConfigureAwait(false);
}
finally
Expand All @@ -485,9 +483,7 @@ await Parallel.ForEachAsync(
},
async (test, ct) =>
{
#pragma warning disable IL2026 // ExecuteTestAsync uses reflection, but caller (ExecuteWithGlobalLimitAsync) is already marked with RequiresUnreferencedCode
test.ExecutionTask ??= _testRunner.ExecuteTestAsync(test, ct);
#pragma warning restore IL2026
test.ExecutionTask ??= _testRunner.ExecuteTestAsync(test, ct).AsTask();
await test.ExecutionTask.ConfigureAwait(false);
}
).ConfigureAwait(false);
Expand Down
32 changes: 10 additions & 22 deletions TUnit.Engine/Services/DataSourceInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ namespace TUnit.Engine.Services;
/// </summary>
internal sealed class DataSourceInitializer
{
private readonly Dictionary<object, Task> _initializationTasks = new();
private readonly object _lock = new();
private readonly ConcurrentDictionary<object, Lazy<Task>> _initializationTasks = new();
private PropertyInjectionService? _propertyInjectionService;

/// <summary>
Expand Down Expand Up @@ -42,30 +41,22 @@ public async Task<T> EnsureInitializedAsync<T>(
}

// Check if already initialized or being initialized
Task? existingTask;
lock (_lock)
{
if (_initializationTasks.TryGetValue(dataSource, out existingTask))
{
// Already initialized or being initialized
}
else
{
// Start initialization
existingTask = InitializeDataSourceAsync(dataSource, objectBag, methodMetadata, events, cancellationToken);
_initializationTasks[dataSource] = existingTask;
}
}
// Use Lazy<Task> to ensure only one initialization task is created per data source (thread-safe)
var lazyTask = _initializationTasks.GetOrAdd(
dataSource,
_ => new Lazy<Task>(() => InitializeDataSourceAsync(dataSource, objectBag, methodMetadata, events, cancellationToken)));

var task = lazyTask.Value;

// Wait for initialization with cancellation support
if (cancellationToken.CanBeCanceled)
{
await existingTask.ConfigureAwait(false);
await task.ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
}
else
{
await existingTask.ConfigureAwait(false);
await task.ConfigureAwait(false);
}

return dataSource;
Expand Down Expand Up @@ -221,9 +212,6 @@ private void CollectNestedObjects(
/// </summary>
public void ClearCache()
{
lock (_lock)
{
_initializationTasks.Clear();
}
_initializationTasks.Clear();
}
}
8 changes: 1 addition & 7 deletions TUnit.Engine/Services/PropertyInjectionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ public async Task InjectPropertiesIntoArgumentsAsync(object?[] arguments, Concur
return;
}

// Fast path: check if any arguments need injection
var injectableArgs = arguments
.Where(argument => argument != null && PropertyInjectionCache.HasInjectableProperties(argument.GetType()))
.ToArray();
Expand All @@ -50,7 +49,6 @@ public async Task InjectPropertiesIntoArgumentsAsync(object?[] arguments, Concur
return;
}

// Process arguments in parallel
var argumentTasks = injectableArgs
.Select(argument => InjectPropertiesIntoObjectAsync(argument!, objectBag, methodMetadata, events))
.ToArray();
Expand Down Expand Up @@ -80,7 +78,6 @@ public Task InjectPropertiesIntoObjectAsync(object instance, ConcurrentDictionar
throw new ArgumentNullException(nameof(events), "TestContextEvents must not be null. Each test permutation must have a unique TestContextEvents instance for proper disposal tracking.");
}

// Start with an empty visited set for cycle detection
#if NETSTANDARD2_0
var visitedObjects = new ConcurrentDictionary<object, byte>();
#else
Expand All @@ -96,8 +93,7 @@ internal async Task InjectPropertiesIntoObjectAsyncCore(object instance, Concurr
return;
}

// Prevent cycles - if we're already processing this object, skip it
// TryAdd returns false if the key already exists (thread-safe)
// Prevent cycles
if (!visitedObjects.TryAdd(instance, 0))
{
return;
Expand All @@ -117,13 +113,11 @@ await PropertyInjectionCache.GetOrAddInjectionTask(instance, async _ =>
{
var plan = PropertyInjectionCache.GetOrCreatePlan(instance.GetType());

// Use the orchestrator for property initialization
await _orchestrator.InitializeObjectWithPropertiesAsync(
instance, plan, objectBag, methodMetadata, events, visitedObjects);
});
}

// After properties are initialized, recursively inject nested properties
await RecurseIntoNestedPropertiesAsync(instance, objectBag, methodMetadata, events, visitedObjects);
}
catch (Exception ex)
Expand Down
42 changes: 12 additions & 30 deletions TUnit.Engine/Services/TestExecution/HashSetPool.cs
Original file line number Diff line number Diff line change
@@ -1,34 +1,26 @@
using System.Collections.Concurrent;

namespace TUnit.Engine.Services.TestExecution;

/// <summary>
/// Thread-safe object pool for HashSet instances used during test execution.
/// Single Responsibility: Managing pooled HashSet objects to reduce allocations.
/// Uses lock-free concurrent collections for high-performance parallel test execution.
/// </summary>
internal sealed class HashSetPool
{
private readonly Dictionary<Type, object> _pools = new();
private readonly object _lock = new();
private readonly ConcurrentDictionary<Type, ConcurrentBag<object>> _pools = new();

public HashSet<T> Rent<T>()
{
var type = typeof(T);
var bag = _pools.GetOrAdd(type, _ => new ConcurrentBag<object>());

lock (_lock)
if (bag.TryTake(out var pooledSet))
{
if (!_pools.TryGetValue(type, out var poolObj))
{
poolObj = new Stack<HashSet<T>>();
_pools[type] = poolObj;
}

var pool = (Stack<HashSet<T>>)poolObj;

if (pool.Count > 0)
{
var set = pool.Pop();
set.Clear();
return set;
}
var set = (HashSet<T>)pooledSet;
set.Clear();
return set;
}

return [];
Expand All @@ -37,19 +29,9 @@ public HashSet<T> Rent<T>()
public void Return<T>(HashSet<T> set)
{
var type = typeof(T);
set.Clear();

lock (_lock)
{
set.Clear();

if (!_pools.TryGetValue(type, out var poolObj))
{
poolObj = new Stack<HashSet<T>>();
_pools[type] = poolObj;
}

var pool = (Stack<HashSet<T>>)poolObj;
pool.Push(set);
}
var bag = _pools.GetOrAdd(type, _ => new ConcurrentBag<object>());
bag.Add(set);
}
}
6 changes: 3 additions & 3 deletions TUnit.Engine/Services/TestExecution/TestCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ public TestCoordinator(
_hashSetPool = hashSetPool;
}

public async Task ExecuteTestAsync(AbstractExecutableTest test, CancellationToken cancellationToken)
public async ValueTask ExecuteTestAsync(AbstractExecutableTest test, CancellationToken cancellationToken)
{
await _executionGuard.TryStartExecutionAsync(test.TestId,
() => ExecuteTestInternalAsync(test, cancellationToken));
() => ExecuteTestInternalAsync(test, cancellationToken).AsTask());
}

private async Task ExecuteTestInternalAsync(AbstractExecutableTest test, CancellationToken cancellationToken)
private async ValueTask ExecuteTestInternalAsync(AbstractExecutableTest test, CancellationToken cancellationToken)
{
try
{
Expand Down
4 changes: 2 additions & 2 deletions TUnit.Engine/TestExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ await _beforeHookTaskCache.GetOrCreateBeforeTestSessionTask(() =>
/// Creates a test executor delegate that wraps the provided executor with hook orchestration.
/// Uses focused services that follow SRP to manage lifecycle and execution.
/// </summary>
public async Task ExecuteAsync(AbstractExecutableTest executableTest, CancellationToken cancellationToken)
public async ValueTask ExecuteAsync(AbstractExecutableTest executableTest, CancellationToken cancellationToken)
{

var testClass = executableTest.Metadata.TestClassType;
Expand Down Expand Up @@ -148,7 +148,7 @@ await _eventReceiverOrchestrator.InvokeFirstTestInClassEventReceiversAsync(
}
}

private static async Task ExecuteTestAsync(AbstractExecutableTest executableTest, CancellationToken cancellationToken)
private static async ValueTask ExecuteTestAsync(AbstractExecutableTest executableTest, CancellationToken cancellationToken)
{
// Skip the actual test invocation for skipped tests
if (executableTest.Context.Metadata.TestDetails.ClassInstance is SkippedTestInstance ||
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Engine/TestInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public TestInitializer(EventReceiverOrchestrator eventReceiverOrchestrator,
_objectTracker = objectTracker;
}

public async Task InitializeTest(AbstractExecutableTest test, CancellationToken cancellationToken)
public async ValueTask InitializeTest(AbstractExecutableTest test, CancellationToken cancellationToken)
{
var testClassInstance = test.Context.Metadata.TestDetails.ClassInstance;

Expand Down
Loading