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
33 changes: 27 additions & 6 deletions TUnit.Engine/Building/Collectors/AotTestDataCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,35 @@ public async Task<IEnumerable<TestMetadata>> CollectTestsAsync(string testSessio
testSourcesByType = testSourcesByType.Where(kvp => filterHints.CouldTypeMatch(kvp.Key));
}

var testSources = testSourcesByType.SelectMany(kvp => kvp.Value);
var testSourcesList = testSourcesByType.SelectMany(kvp => kvp.Value).ToList();

var standardTestMetadatas = await testSources
.SelectManyAsync(testSource => testSource.GetTestsAsync(testSessionId))
.ProcessInParallel();
// Use sequential processing for small test source sets to avoid task scheduling overhead
IEnumerable<TestMetadata> standardTestMetadatas;
if (testSourcesList.Count < Building.ParallelThresholds.MinItemsForParallel)
{
var results = new List<TestMetadata>();
foreach (var testSource in testSourcesList)
{
await foreach (var metadata in testSource.GetTestsAsync(testSessionId))
{
results.Add(metadata);
}
}
standardTestMetadatas = results;
}
else
{
standardTestMetadatas = await testSourcesList
.SelectManyAsync(testSource => testSource.GetTestsAsync(testSessionId))
.ProcessInParallel();
}

var dynamicTestMetadatas = await CollectDynamicTestsStreaming(testSessionId)
.ProcessInParallel();
// Dynamic tests are typically rare, collect sequentially
var dynamicTestMetadatas = new List<TestMetadata>();
await foreach (var metadata in CollectDynamicTestsStreaming(testSessionId))
{
dynamicTestMetadatas.Add(metadata);
}

return [..standardTestMetadatas, ..dynamicTestMetadatas];
}
Expand Down
64 changes: 56 additions & 8 deletions TUnit.Engine/Building/TestBuilderPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@

namespace TUnit.Engine.Building;

/// <summary>
/// Threshold below which sequential processing is used instead of parallel.
/// For small test sets, the overhead of task scheduling exceeds parallelization benefits.
/// </summary>
internal static class ParallelThresholds
{
/// <summary>
/// Minimum number of items before parallel processing is used.
/// Below this threshold, sequential processing avoids task scheduling overhead.
/// </summary>
public const int MinItemsForParallel = 8;
}

internal sealed class TestBuilderPipeline
{
private readonly ITestDataCollector _dataCollector;
Expand Down Expand Up @@ -137,25 +150,60 @@ public async Task<IEnumerable<AbstractExecutableTest>> BuildTestsFromMetadataAsy
TestBuildingContext buildingContext,
CancellationToken cancellationToken = default)
{
var testGroups = await testMetadata.SelectAsync(async metadata =>
// Materialize to check count - for small sets, sequential processing is faster
var metadataList = testMetadata as IList<TestMetadata> ?? testMetadata.ToList();

IEnumerable<IEnumerable<AbstractExecutableTest>> testGroups;

if (metadataList.Count < ParallelThresholds.MinItemsForParallel)
{
// Sequential processing for small sets - avoids task scheduling overhead
var results = new List<IEnumerable<AbstractExecutableTest>>(metadataList.Count);
foreach (var metadata in metadataList)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
// Check if this is a dynamic test metadata that should bypass normal test building
if (metadata is IDynamicTestMetadata)
{
return await GenerateDynamicTests(metadata).ConfigureAwait(false);
results.Add(await GenerateDynamicTests(metadata).ConfigureAwait(false));
}
else
{
results.Add(await _testBuilder.BuildTestsFromMetadataAsync(metadata, buildingContext).ConfigureAwait(false));
}

return await _testBuilder.BuildTestsFromMetadataAsync(metadata, buildingContext).ConfigureAwait(false);
}
catch (Exception ex)
{
var failedTest = CreateFailedTestForDataGenerationError(metadata, ex);
return [failedTest];
results.Add([failedTest]);
}
}, cancellationToken: cancellationToken)
.ProcessInParallel(Environment.ProcessorCount);
}
testGroups = results;
}
else
{
// Parallel processing for larger sets
testGroups = await metadataList.SelectAsync(async metadata =>
{
try
{
// Check if this is a dynamic test metadata that should bypass normal test building
if (metadata is IDynamicTestMetadata)
{
return await GenerateDynamicTests(metadata).ConfigureAwait(false);
}

return await _testBuilder.BuildTestsFromMetadataAsync(metadata, buildingContext).ConfigureAwait(false);
}
catch (Exception ex)
{
var failedTest = CreateFailedTestForDataGenerationError(metadata, ex);
return (IEnumerable<AbstractExecutableTest>)[failedTest];
}
}, cancellationToken: cancellationToken)
.ProcessInParallel(Environment.ProcessorCount);
}

return testGroups.SelectMany(x => x);
}
Expand Down
Loading