From 39b33be13e6102538b01539c4efd1c74fc5c8755 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 4 Nov 2026 01:56:17 +0000 Subject: [PATCH] Remove unused streaming test builder path (#7327) --- .../Building/Interfaces/ITestBuilder.cs | 13 - src/TUnit.Engine/Building/TestBuilder.cs | 336 ------------------ tests/TUnit.UnitTests/ClassRowCleanupTests.cs | 52 +-- 3 files changed, 9 insertions(+), 392 deletions(-) diff --git a/src/TUnit.Engine/Building/Interfaces/ITestBuilder.cs b/src/TUnit.Engine/Building/Interfaces/ITestBuilder.cs index 9ed53a3db5f..99c00b828f1 100644 --- a/src/TUnit.Engine/Building/Interfaces/ITestBuilder.cs +++ b/src/TUnit.Engine/Building/Interfaces/ITestBuilder.cs @@ -29,19 +29,6 @@ internal interface ITestBuilder #endif ValueTask> BuildTestsFromMetadataAsync(TestMetadata metadata, TestBuildingContext buildingContext, CancellationToken cancellationToken = default); - /// - /// Streaming version that yields tests as they're built without buffering - /// - /// The test metadata with DataCombinationGenerator - /// Cancellation token - /// Stream of executable tests for all data combinations -#if NET8_0_OR_GREATER - [RequiresUnreferencedCode("Test building in reflection mode uses generic type resolution which requires unreferenced code")] -#endif - IAsyncEnumerable BuildTestsStreamingAsync( - TestMetadata metadata, - CancellationToken cancellationToken = default); - /// /// Invokes event receivers after dependencies have been resolved. /// This populates TestContext._dependencies and fires ITestRegisteredEventReceiver diff --git a/src/TUnit.Engine/Building/TestBuilder.cs b/src/TUnit.Engine/Building/TestBuilder.cs index dc8a3abe4e0..8cc14cb7680 100644 --- a/src/TUnit.Engine/Building/TestBuilder.cs +++ b/src/TUnit.Engine/Building/TestBuilder.cs @@ -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 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 - var classConstructorAttribute = attributes.FirstOfType(); - - 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 @@ -2541,160 +2359,6 @@ private async Task CreateInstanceForMethodDataSources( } } -#if NET8_0_OR_GREATER - [RequiresUnreferencedCode("Generic type resolution for test building uses reflection")] -#endif - private async Task BuildSingleTestAsync( - TestMetadata metadata, - Func> classDataFactory, - UnclaimedClassRow unclaimedClassRow, - Func> 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(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] to the class, or use a data source (like [ClassDataSource] 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> instanceFactory; - - if (basicSkipReason is { Length: > 0 }) - { - instanceFactory = () => Task.FromResult(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); - } - } - /// /// 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. diff --git a/tests/TUnit.UnitTests/ClassRowCleanupTests.cs b/tests/TUnit.UnitTests/ClassRowCleanupTests.cs index fbee18abc1c..593a25c772a 100644 --- a/tests/TUnit.UnitTests/ClassRowCleanupTests.cs +++ b/tests/TUnit.UnitTests/ClassRowCleanupTests.cs @@ -138,9 +138,7 @@ public async Task RejectedMethodRowsRetainCleanupWithoutLifecycleSubscriptions(b } [Test] - [Arguments(false)] - [Arguments(true)] - public async Task InstanceConstructorSubscriptionsReachExecutableRow(bool streaming) + public async Task InstanceConstructorSubscriptionsReachExecutableRow() { var current = TestContext.Current!; var builder = (ITestBuilder)current.ServiceProvider.GetService(typeof(ITestBuilder))!; @@ -153,7 +151,7 @@ public async Task InstanceConstructorSubscriptionsReachExecutableRow(bool stream LineNumber = 0, AttributeFactory = () => [new ClassConstructorAttribute()], MethodMetadata = current.TestDetails.MethodMetadata, - DataSources = [new ArgumentsAttribute(streaming)], + DataSources = [], ClassDataSources = [], PropertyDataSources = [], InstanceFactory = (_, _) => new ClassRowCleanupTests(), @@ -162,15 +160,7 @@ public async Task InstanceConstructorSubscriptionsReachExecutableRow(bool stream var tests = new List(); try { - if (streaming) - { - await foreach (var test in builder.BuildTestsStreamingAsync(metadata)) - tests.Add(test); - } - else - { - tests.AddRange(await builder.BuildTestsFromMetadataAsync(metadata, new TestBuildingContext(false, null))); - } + tests.AddRange(await builder.BuildTestsFromMetadataAsync(metadata, new TestBuildingContext(false, null))); await Assert.That(tests.Count).IsEqualTo(1); var executable = tests[0]; await executable.CreateInstanceAsync(); @@ -507,9 +497,7 @@ private sealed class ThrowingClassSource : Attribute, IDataSourceAttribute } [Test] - [Arguments(false)] - [Arguments(true)] - public async Task ConstructorFactoryStateIsIsolatedAcrossRepeats(bool streaming) + public async Task ConstructorFactoryStateIsIsolatedAcrossRepeats() { var current = TestContext.Current!; var builder = (ITestBuilder)current.ServiceProvider.GetService(typeof(ITestBuilder))!; @@ -533,17 +521,7 @@ public async Task ConstructorFactoryStateIsIsolatedAcrossRepeats(bool streaming) var tests = new List(); try { - if (streaming) - { - await foreach (var test in builder.BuildTestsStreamingAsync(metadata)) - { - tests.Add(test); - } - } - else - { - tests.AddRange(await builder.BuildTestsFromMetadataAsync(metadata, new TestBuildingContext(false, null))); - } + tests.AddRange(await builder.BuildTestsFromMetadataAsync(metadata, new TestBuildingContext(false, null))); await Assert.That(tests.Count).IsEqualTo(3); await Assert.That(source.Resources.Count).IsEqualTo(3); for (var i = 0; i < tests.Count; i++) @@ -563,11 +541,9 @@ public async Task ConstructorFactoryStateIsIsolatedAcrossRepeats(bool streaming) } [Test] - [Arguments(false, false)] - [Arguments(false, true)] - [Arguments(true, false)] - [Arguments(true, true)] - public async Task ClassAndMethodEnumerationStateSurvivesFactories(bool streaming, bool classEnumeration) + [Arguments(false)] + [Arguments(true)] + public async Task ClassAndMethodEnumerationStateSurvivesFactories(bool classEnumeration) { var current = TestContext.Current!; var builder = (ITestBuilder)current.ServiceProvider.GetService(typeof(ITestBuilder))!; @@ -590,17 +566,7 @@ public async Task ClassAndMethodEnumerationStateSurvivesFactories(bool streaming var tests = new List(); try { - if (streaming) - { - await foreach (var test in builder.BuildTestsStreamingAsync(metadata)) - { - tests.Add(test); - } - } - else - { - tests.AddRange(await builder.BuildTestsFromMetadataAsync(metadata, new TestBuildingContext(false, null))); - } + tests.AddRange(await builder.BuildTestsFromMetadataAsync(metadata, new TestBuildingContext(false, null))); await Assert.That(tests.Count).IsEqualTo(4); foreach (var test in tests)