Skip to content
Merged
Changes from 1 commit
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
51 changes: 30 additions & 21 deletions TUnit.Engine/Building/Collectors/AotTestDataCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,29 +39,19 @@ public Task<IEnumerable<TestMetadata>> CollectTestsAsync(string testSessionId)
#endif
public async Task<IEnumerable<TestMetadata>> CollectTestsAsync(string testSessionId, ITestExecutionFilter? filter)
{
// Extract hints from filter for pre-filtering test sources by type
var filterHints = MetadataFilterMatcher.ExtractFilterHints(filter);

// Try two-phase discovery with single-pass filtering when all sources support descriptors.
// This avoids double-enumeration: previously ExpandSourcesForDependencies enumerated
// descriptors to find dependencies, then CollectTestsWithTwoPhaseDiscoveryAsync enumerated
// them again. Now we pass ALL sources and do type-level + descriptor-level filtering
// in a single pass, while indexing everything for dependency resolution.
IEnumerable<TestMetadata> standardTestMetadatas;

if (filterHints.HasHints && Sources.TestSources.All(static kvp => kvp.Value.All(static s => s is ITestDescriptorSource)))
{
// Single-pass two-phase discovery: enumerate all descriptors once,
// apply type and descriptor filters during enumeration, expand dependencies from index
// Filtered: enumerate descriptors, apply filters, expand dependencies, materialize matches
standardTestMetadatas = CollectTestsWithTwoPhaseDiscovery(
Sources.TestSources,
testSessionId,
filterHints);
}
else
{
// Fallback: Use traditional collection (for legacy sources or no filter hints)
// Apply type-level pre-filtering when hints are available
IEnumerable<KeyValuePair<Type, ConcurrentQueue<ITestSource>>> testSourcesByType = Sources.TestSources;

if (filterHints.HasHints)
Expand All @@ -70,7 +60,7 @@ public async Task<IEnumerable<TestMetadata>> CollectTestsAsync(string testSessio
}

var testSourcesList = testSourcesByType.SelectMany(kvp => kvp.Value).ToList();
standardTestMetadatas = CollectTestsTraditional(testSourcesList, testSessionId);
standardTestMetadatas = CollectTests(testSourcesList, testSessionId);
}

// Dynamic tests are typically rare, collect sequentially
Expand Down Expand Up @@ -221,23 +211,42 @@ private IEnumerable<TestMetadata> CollectTestsWithTwoPhaseDiscovery(
}

/// <summary>
/// Traditional collection: materialize all tests from sources.
/// Used when filter hints are not available or sources don't support ITestDescriptorSource.
/// Materializes TestMetadata from every source.
/// Uses parallel processing for large source sets to avoid sequential JIT + metadata creation bottleneck.
/// </summary>
private IEnumerable<TestMetadata> CollectTestsTraditional(
private IEnumerable<TestMetadata> CollectTests(
List<ITestSource> testSourcesList,
string testSessionId)
{
var results = new List<TestMetadata>();
foreach (var testSource in testSourcesList)
if (testSourcesList.Count < ParallelThresholds.MinItemsForParallel)
{
var tests = testSource.GetTests(testSessionId);
for (var i = 0; i < tests.Count; i++)
var results = new List<TestMetadata>();
foreach (var testSource in testSourcesList)
{
results.Add(tests[i]);
var tests = testSource.GetTests(testSessionId);
for (var i = 0; i < tests.Count; i++)
{
results.Add(tests[i]);
}
}
return results;
}
return results;

var allResults = new ConcurrentBag<IReadOnlyList<TestMetadata>>();
Parallel.ForEach(testSourcesList, testSource =>
{
allResults.Add(testSource.GetTests(testSessionId));
});

var combined = new List<TestMetadata>();
foreach (var batch in allResults)
{
for (var i = 0; i < batch.Count; i++)
{
combined.Add(batch[i]);
}
}
return combined;
}

[RequiresUnreferencedCode("Dynamic test collection requires expression compilation and reflection")]
Expand Down
Loading