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
13 changes: 0 additions & 13 deletions src/TUnit.Engine/Building/Interfaces/ITestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,6 @@ internal interface ITestBuilder
#endif
ValueTask<IEnumerable<AbstractExecutableTest>> BuildTestsFromMetadataAsync(TestMetadata metadata, TestBuildingContext buildingContext, CancellationToken cancellationToken = default);

/// <summary>
/// Streaming version that yields tests as they're built without buffering
/// </summary>
/// <param name="metadata">The test metadata with DataCombinationGenerator</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Stream of executable tests for all data combinations</returns>
#if NET8_0_OR_GREATER
[RequiresUnreferencedCode("Test building in reflection mode uses generic type resolution which requires unreferenced code")]
#endif
IAsyncEnumerable<AbstractExecutableTest> BuildTestsStreamingAsync(
TestMetadata metadata,
CancellationToken cancellationToken = default);

/// <summary>
/// Invokes event receivers after dependencies have been resolved.
/// This populates TestContext._dependencies and fires ITestRegisteredEventReceiver
Expand Down
336 changes: 0 additions & 336 deletions src/TUnit.Engine/Building/TestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2249,188 +2249,6 @@ private InstanceCreationResult(object? instance, Exception? exception)
public static InstanceCreationResult CreateFailure(Exception exception) => new(null, exception);
}

#if NET8_0_OR_GREATER
[RequiresUnreferencedCode("Test building in reflection mode uses generic type resolution which requires unreferenced code")]
#endif
public async IAsyncEnumerable<AbstractExecutableTest> BuildTestsStreamingAsync(
TestMetadata metadata,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Handle GenericTestMetadata with ConcreteInstantiations
if (metadata is GenericTestMetadata { ConcreteInstantiations.Count: > 0 } genericMetadata)
{
// Stream tests from each concrete instantiation
foreach (var concreteMetadata in genericMetadata.ConcreteInstantiations.Values)
{
await foreach (var test in BuildTestsStreamingAsync(concreteMetadata, cancellationToken))
{
yield return test;
}
}
yield break;
}

// Use pre-extracted repeat count from metadata (avoids instantiating attributes)
var repeatCount = metadata.RepeatCount ?? 0;

// Initialize attributes
var attributes = await InitializeAttributesAsync(metadata.GetOrCreateAttributes(), cancellationToken);

// Create base context with ClassConstructor if present
// StateBag and Events are lazy-initialized for performance
var baseContext = new TestBuilderContext
{
TestMetadata = metadata.MethodMetadata,
InitializedAttributes = attributes // Store the initialized attributes
};

// Set the static AsyncLocal immediately so it's available for property data sources
// This must be set BEFORE any operations that might invoke data source methods
TestBuilderContext.Current = baseContext;

// Check for ClassConstructor attribute and set it early if present
// Look for any attribute that inherits from ClassConstructorAttribute
// This handles both ClassConstructorAttribute and ClassConstructorAttribute<T>
var classConstructorAttribute = attributes.FirstOfType<ClassConstructorAttribute>();

if (classConstructorAttribute != null)
{
baseContext.ClassConstructor = (IClassConstructor)Activator.CreateInstance(classConstructorAttribute.ClassConstructorType)!;
}

var contextAccessor = new TestBuilderContextAccessor(baseContext);

// Check for circular dependency
if (HasInstanceDataAccessor(metadata.ClassDataSources))
{
yield return CreateFailedTestForClassDataSourceCircularDependency(metadata);
yield break;
}

// Stream through all data source combinations
var classDataAttributeIndex = 0;
foreach (var classDataSource in await GetDataSourcesAsync(metadata.ClassDataSources, cancellationToken))
{
var classSourceContext = baseContext.CreateChild(classDataSource, null);
contextAccessor.Current = classSourceContext;
classDataAttributeIndex++;
var classDataLoopIndex = 0;

await foreach (var classDataFactory in GetInitializedDataRowsAsync(
classDataSource,
metadata,
generatorType: DataGeneratorType.ClassParameters,
testClassInstance: null,
classInstanceArguments: null,
contextAccessor,
cancellationToken).WithCancellation(cancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
classDataLoopIndex++;

contextAccessor.Current = classSourceContext.CreateChild(classDataSource, null);

// Keep constructor-factory state on its saved row, outside the enumeration baseline.
var classFactoryContext = contextAccessor.Current;
var classRowEnumerationContext = classFactoryContext.CreateChild(classDataSource, null);
var (classData, firstClassRowMetadata) = DataUnwrapper.UnwrapWithMetadata(await classDataFactory() ?? []);

// The first test built from this class row takes this fetch instead of calling the factory
// again, so a non-shared source does not create an extra, untracked instance (#7157).
var unclaimedClassRow = new UnclaimedClassRow(classData, firstClassRowMetadata, contextAccessor.Current, classRowEnumerationContext);
contextAccessor.Current = classRowEnumerationContext;

// Initialize objects before method data sources are evaluated (ObjectInitializer is phase-aware)
await InitializeClassDataAsync(classData);

// Handle instance creation for method data sources
var needsInstanceForMethodDataSources = HasInstanceDataAccessor(metadata.DataSources);
object? instanceForMethodDataSources = null;

try
{
if (needsInstanceForMethodDataSources)
{
// The temporary instance is built from the first fetch and disposed below, so no
// test may receive those objects: it could find them disposed. The next test
// tracks them instead, so they are still disposed.
unclaimedClassRow.LendToTemporaryInstance();

var instanceResult = await CreateInitializedInstanceForMethodDataSources(
metadata,
classDataAttributeIndex,
classDataLoopIndex,
classData,
classFactoryContext,
cancellationToken);

if (!instanceResult.Success)
{
unclaimedClassRow.TransferCleanupTo(contextAccessor.Current);
// Yield a failed test instead of silently skipping
yield return CreateFailedTestForInstanceDataSourceError(metadata, instanceResult.Exception!, classData, contextAccessor.Current);
continue;
}

instanceForMethodDataSources = instanceResult.Instance!;
}

// Stream through method data sources
var methodDataAttributeIndex = 0;
foreach (var methodDataSource in await GetDataSourcesAsync(metadata.DataSources, cancellationToken))
{
methodDataAttributeIndex++;
var methodDataLoopIndex = 0;

await foreach (var methodDataFactory in GetInitializedDataRowsAsync(
methodDataSource,
metadata,
generatorType: DataGeneratorType.TestParameters,
testClassInstance: methodDataSource is IAccessesInstanceData ? instanceForMethodDataSources : null,
classInstanceArguments: classData,
contextAccessor,
cancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
methodDataLoopIndex++;

var enumerationContext = contextAccessor.Current;
// Stream through repeat count
for (var i = 0; i < repeatCount + 1; i++)
{
cancellationToken.ThrowIfCancellationRequested();

contextAccessor.Current = enumerationContext.CreateChild(classDataSource, methodDataSource);

// Build and yield single test
var test = await BuildSingleTestAsync(
metadata, classDataFactory, unclaimedClassRow, methodDataFactory,
classDataAttributeIndex, classDataLoopIndex,
methodDataAttributeIndex, methodDataLoopIndex,
i, contextAccessor,
classDataSource, methodDataSource,
cancellationToken);

if (test != null)
{
yield return test;
}
}

// Keep subsequent rows and repeats independent of each test's factory state.
contextAccessor.Current = enumerationContext.CreateChild(classDataSource, methodDataSource);
}
}
}
finally
{
await DisposeInstanceForMethodDataSourcesAsync(instanceForMethodDataSources);
contextAccessor.Current = classSourceContext;
}
}
}
}

#if NET8_0_OR_GREATER
[RequiresUnreferencedCode("Generic type resolution for instance creation uses reflection")]
#endif
Expand Down Expand Up @@ -2541,160 +2359,6 @@ private async Task<InstanceCreationResult> CreateInstanceForMethodDataSources(
}
}

#if NET8_0_OR_GREATER
[RequiresUnreferencedCode("Generic type resolution for test building uses reflection")]
#endif
private async Task<AbstractExecutableTest?> BuildSingleTestAsync(
TestMetadata metadata,
Func<Task<object?[]?>> classDataFactory,
UnclaimedClassRow unclaimedClassRow,
Func<Task<object?[]?>> methodDataFactory,
int classDataAttributeIndex,
int classDataLoopIndex,
int methodDataAttributeIndex,
int methodDataLoopIndex,
int repeatIndex,
TestBuilderContextAccessor contextAccessor,
IDataSourceAttribute? classDataSource = null,
IDataSourceAttribute? methodDataSource = null,
CancellationToken cancellationToken = default)
{
// Declared outside the try so that a row which fails after its factories ran still hands its
// objects to the failed test, which tracks them for disposal (#7123).
object?[]? classData = null;
object?[]? methodData = null;

try
{
(classData, var classRowMetadata) = await unclaimedClassRow.TakeOrFetchAsync(classDataFactory, contextAccessor.Current);
(methodData, var methodRowMetadata) = DataUnwrapper.UnwrapWithTypesAndMetadata(await methodDataFactory() ?? [], metadata.MethodMetadata.Parameters);

// Extract and merge metadata from data source attributes and TestDataRow wrappers
var classAttrMetadata = DataSourceMetadataExtractor.ExtractFromAttribute(classDataSource);
var methodAttrMetadata = DataSourceMetadataExtractor.ExtractFromAttribute(methodDataSource);
var mergedClassMetadata = DataSourceMetadataExtractor.Merge(classRowMetadata, classAttrMetadata);
var mergedMethodMetadata = DataSourceMetadataExtractor.Merge(methodRowMetadata, methodAttrMetadata);
var finalMetadata = DataSourceMetadataExtractor.Merge(mergedMethodMetadata, mergedClassMetadata);

// Initialize method data objects (ObjectInitializer is phase-aware)
await InitializeClassDataAsync(methodData);

// Check data compatibility for generic methods
if (metadata.GenericMethodTypeArguments is { Length: > 0 })
{
if (!IsDataCompatibleWithExpectedTypes(metadata, methodData))
{
// Skip incompatible data. No test holds the class row's objects, so the next test takes them.
unclaimedClassRow.Return(classData, classRowMetadata, contextAccessor.Current);
return null;
}
}

var tempTestData = new TestData
{
TestClassInstanceFactory = () => Task.FromResult<object>(null!),
ClassDataSourceAttributeIndex = classDataAttributeIndex,
ClassDataLoopIndex = classDataLoopIndex,
ClassData = classData,
MethodDataSourceAttributeIndex = methodDataAttributeIndex,
MethodDataLoopIndex = methodDataLoopIndex,
MethodData = methodData,
RepeatIndex = repeatIndex,
InheritanceDepth = metadata.InheritanceDepth
};

// Resolve generic types
Type[] resolvedClassGenericArgs;
Type[] resolvedMethodGenericArgs;

try
{
var resolution = TestGenericTypeResolver.Resolve(metadata, tempTestData);
resolvedClassGenericArgs = resolution.ResolvedClassGenericArguments;
resolvedMethodGenericArgs = resolution.ResolvedMethodGenericArguments;
}
catch (GenericTypeResolutionException) when (
metadata.TestClassType.IsGenericTypeDefinition &&
classData.Length == 0 &&
methodData.Length > 0)
{
try
{
resolvedClassGenericArgs = TryInferClassGenericsFromMethodData(metadata, methodData);
resolvedMethodGenericArgs = Type.EmptyTypes;
}
catch (Exception ex)
{
return CreateFailedTestForDataRowError(metadata, ex, classDataAttributeIndex, classDataLoopIndex, methodDataAttributeIndex, methodDataLoopIndex, repeatIndex, unclaimedClassRow.WithLentObjects(classData), methodData, contextAccessor.Current);
}
}
catch (Exception ex)
{
return CreateFailedTestForDataRowError(metadata, ex, classDataAttributeIndex, classDataLoopIndex, methodDataAttributeIndex, methodDataLoopIndex, repeatIndex, unclaimedClassRow.WithLentObjects(classData), methodData, contextAccessor.Current);
}

if (metadata.TestClassType.IsGenericTypeDefinition && resolvedClassGenericArgs.Length == 0)
{
throw new InvalidOperationException($"Cannot create test for generic class '{metadata.TestClassType.Name}': No type arguments could be inferred. Add [GenerateGenericTest<ConcreteType>] to the class, or use a data source (like [ClassDataSource<T>] or [Arguments]) that provides constructor arguments to infer the generic type arguments from.");
}

// Create instance factory
var attributes = contextAccessor.Current.InitializedAttributes ?? [];
var basicSkipReason = GetBasicSkipReason(metadata, attributes);
Func<Task<object>> instanceFactory;

if (basicSkipReason is { Length: > 0 })
{
instanceFactory = () => Task.FromResult<object>(SkippedTestInstance.Instance);
}
else
{
var capturedMetadata = metadata;
var capturedClassGenericArgs = resolvedClassGenericArgs;
var capturedClassData = classData;
var capturedContext = contextAccessor.Current;
instanceFactory = () => CreateInstance(capturedMetadata, capturedClassGenericArgs, capturedClassData, capturedContext);
}

// Build final test data
var testData = new TestData
{
TestClassInstanceFactory = instanceFactory,
ClassDataSourceAttributeIndex = classDataAttributeIndex,
ClassDataLoopIndex = classDataLoopIndex,
ClassData = classData,
MethodDataSourceAttributeIndex = methodDataAttributeIndex,
MethodDataLoopIndex = methodDataLoopIndex,
MethodData = methodData,
RepeatIndex = repeatIndex,
InheritanceDepth = metadata.InheritanceDepth,
ResolvedClassGenericArguments = resolvedClassGenericArgs,
ResolvedMethodGenericArguments = resolvedMethodGenericArgs,
Metadata = finalMetadata,
UnreceivedClassRowObjects = unclaimedClassRow.TakeLentObjects()
};

// Events is lazy-initialized; explicitly share StateBag from per-iteration context
var testSpecificContext = contextAccessor.Current.CreateChild(classDataSource ?? contextAccessor.Current.ClassDataSourceAttribute, methodDataSource ?? contextAccessor.Current.DataSourceAttribute, shareFactoryContext: true);

var test = await BuildTestAsync(metadata, testData, testSpecificContext, cancellationToken: cancellationToken);

if (!string.IsNullOrEmpty(basicSkipReason))
{
test.Context.SkipReason = basicSkipReason;
}

return test;
}
catch (Exception ex)
{
// Includes a GenericConstraintViolationDataSource row whose inferred type arguments break the
// method's constraints. The row's indexes keep its id distinct from the method's other rows.
var (reportedException, rowData) = UnpackConstraintViolation(ex, metadata.MethodMetadata.Parameters);
return CreateFailedTestForDataRowError(metadata, reportedException, classDataAttributeIndex, classDataLoopIndex, methodDataAttributeIndex, methodDataLoopIndex, repeatIndex, unclaimedClassRow.WithLentObjects(classData), rowData ?? methodData, contextAccessor.Current);
}
}

/// <summary>
/// Determines if a test could potentially match the filter without building the full test object.
/// This is a conservative check - returns true unless we can definitively rule out the test.
Expand Down
Loading
Loading