diff --git a/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs b/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs index 6a20095a97..29e833e178 100644 --- a/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs @@ -38,6 +38,28 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } GenerateIndividualPropertyInjectionSource(context, classData); }); + + // Also generate property sources for closed generic types used in data source attributes + // This ensures AOT compatibility for types like ErrFixture + var closedGenericTypesFromDataSources = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (node, _) => IsClassWithDataSourceProperties(node), + transform: (ctx, _) => GetClosedGenericTypesFromDataSources(ctx)) + .Where(x => x != null) + .SelectMany((types, _) => types ?? []) + .Collect() + .SelectMany((classes, _) => classes.DistinctBy(c => c.ClassSymbol, SymbolEqualityComparer.Default)) + .Combine(enabledProvider); + + context.RegisterSourceOutput(closedGenericTypesFromDataSources, (context, data) => + { + var (classData, isEnabled) = data; + if (!isEnabled) + { + return; + } + GenerateIndividualPropertyInjectionSource(context, classData); + }); } private static bool IsClassWithDataSourceProperties(SyntaxNode node) @@ -45,6 +67,130 @@ private static bool IsClassWithDataSourceProperties(SyntaxNode node) return node is TypeDeclarationSyntax; } + /// + /// Extracts closed generic types from data source attributes (like ClassDataSource<ErrFixture<MyType>>) + /// that have injectable properties. This enables AOT-compatible property injection for generic types. + /// + private static IEnumerable? GetClosedGenericTypesFromDataSources(GeneratorSyntaxContext context) + { + var typeDecl = (TypeDeclarationSyntax)context.Node; + var semanticModel = context.SemanticModel; + + if (semanticModel.GetDeclaredSymbol(typeDecl) is not INamedTypeSymbol typeSymbol) + { + return null; + } + + var dataSourceInterface = semanticModel.Compilation.GetTypeByMetadataName("TUnit.Core.IDataSourceAttribute"); + if (dataSourceInterface == null) + { + return null; + } + + var closedGenericTypes = new List(); + var processedTypes = new HashSet(); + + // Check all properties for data source attributes that reference closed generic types + var allProperties = typeSymbol.GetMembers() + .OfType() + .Concat(typeSymbol.GetMembersIncludingBase().OfType()); + + foreach (var property in allProperties) + { + foreach (var attr in property.GetAttributes()) + { + if (attr.AttributeClass == null || + !attr.AttributeClass.AllInterfaces.Contains(dataSourceInterface, SymbolEqualityComparer.Default)) + { + continue; + } + + // Check if the attribute is a generic type like ClassDataSource + if (attr.AttributeClass.IsGenericType && attr.AttributeClass.TypeArguments.Length > 0) + { + foreach (var typeArg in attr.AttributeClass.TypeArguments) + { + if (typeArg is INamedTypeSymbol namedTypeArg && namedTypeArg.IsGenericType && + !namedTypeArg.IsUnboundGenericType && namedTypeArg.TypeParameters.Length == 0) + { + // This is a closed generic type (e.g., ErrFixture) + var fullName = namedTypeArg.ToDisplayString(); + if (!processedTypes.Add(fullName)) + { + continue; + } + + // Check if this type has properties with data source attributes + var classData = GetClassWithDataSourcePropertiesForType(namedTypeArg, semanticModel, dataSourceInterface); + if (classData != null) + { + closedGenericTypes.Add(classData); + } + } + } + } + } + } + + return closedGenericTypes.Count > 0 ? closedGenericTypes : null; + } + + /// + /// Creates a ClassWithDataSourceProperties for a specific type (used for closed generic types). + /// + private static ClassWithDataSourceProperties? GetClassWithDataSourcePropertiesForType( + INamedTypeSymbol typeSymbol, + SemanticModel semanticModel, + INamedTypeSymbol dataSourceInterface) + { + // Skip types that are not publicly accessible + if (!IsPubliclyAccessible(typeSymbol)) + { + return null; + } + + var propertiesWithDataSources = new List(); + var processedProperties = new HashSet(); + + var allProperties = typeSymbol.GetMembers() + .OfType() + .Where(CanSetProperty) + .ToList(); + + foreach (var property in allProperties) + { + if (!processedProperties.Add(property.Name)) + { + continue; + } + + foreach (var attr in property.GetAttributes()) + { + if (attr.AttributeClass != null && + attr.AttributeClass.AllInterfaces.Contains(dataSourceInterface, SymbolEqualityComparer.Default)) + { + propertiesWithDataSources.Add(new PropertyWithDataSourceAttribute + { + Property = property, + DataSourceAttribute = attr + }); + break; + } + } + } + + if (propertiesWithDataSources.Count == 0) + { + return null; + } + + return new ClassWithDataSourceProperties + { + ClassSymbol = typeSymbol, + Properties = propertiesWithDataSources.ToImmutableArray() + }; + } + private static ClassWithDataSourceProperties? GetClassWithDataSourceProperties(GeneratorSyntaxContext context) { var typeDecl = (TypeDeclarationSyntax)context.Node; diff --git a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs index 8c5f623eb9..be1bbd1320 100644 --- a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs @@ -937,15 +937,23 @@ private static void GenerateMethodDataSourceAttribute(CodeWriter writer, Attribu // Generate the attribute with factory // We need to manually construct this to properly add the Factory property - var attrClass = attr.AttributeClass!; - var attrTypeName = attrClass.GloballyQualified(); + + // Determine if the data source is static or instance-based + var isStatic = dataSourceMethod?.IsStatic ?? dataSourceProperty?.GetMethod?.IsStatic ?? true; + + // Use InstanceMethodDataSourceAttribute for instance-based data sources + // This implements IAccessesInstanceData which tells the engine to create an instance early + var attrTypeName = isStatic + ? "global::TUnit.Core.MethodDataSourceAttribute" + : "global::TUnit.Core.InstanceMethodDataSourceAttribute"; if (attr.ConstructorArguments is [ { Value: ITypeSymbol typeArg } _, _, .. ]) { - // MethodDataSource(Type, string) constructor + // MethodDataSource(Type, string) constructor - only available on MethodDataSourceAttribute + // For instance data sources, we still use the same constructor signature writer.AppendLine($"new {attrTypeName}(typeof({typeArg.GloballyQualified()}), \"{methodName}\")"); } else @@ -1048,7 +1056,7 @@ private static void GenerateMethodDataSourceFactory(CodeWriter writer, IMethodSy writer.AppendLine("else"); writer.AppendLine("{"); writer.Indent(); - InstanceFactoryGenerator.GenerateInstanceCreation(writer, targetType, "instance"); + writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");"); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine($"var result = (({fullyQualifiedType})instance).{methodCall};"); @@ -1080,7 +1088,7 @@ private static void GenerateMethodDataSourceFactory(CodeWriter writer, IMethodSy writer.AppendLine("else"); writer.AppendLine("{"); writer.Indent(); - InstanceFactoryGenerator.GenerateInstanceCreation(writer, targetType, "instance"); + writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");"); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine($"var result = (({fullyQualifiedType})instance).{methodCall};"); @@ -1124,7 +1132,7 @@ private static void GenerateMethodDataSourceFactory(CodeWriter writer, IMethodSy writer.AppendLine("else"); writer.AppendLine("{"); writer.Indent(); - InstanceFactoryGenerator.GenerateInstanceCreation(writer, targetType, "instance"); + writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");"); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine($"var result = (({fullyQualifiedType})instance).{methodCall};"); @@ -1167,7 +1175,7 @@ private static void GenerateMethodDataSourceFactory(CodeWriter writer, IMethodSy writer.AppendLine("else"); writer.AppendLine("{"); writer.Indent(); - InstanceFactoryGenerator.GenerateInstanceCreation(writer, targetType, "instance"); + writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");"); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine($"var result = (({fullyQualifiedType})instance).{methodCall};"); @@ -1217,7 +1225,7 @@ private static void GeneratePropertyDataSourceFactory(CodeWriter writer, IProper writer.AppendLine("else"); writer.AppendLine("{"); writer.Indent(); - InstanceFactoryGenerator.GenerateInstanceCreation(writer, targetType, "instance"); + writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");"); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine($"var result = (({fullyQualifiedType})instance).{propertyAccess};"); @@ -1249,7 +1257,7 @@ private static void GeneratePropertyDataSourceFactory(CodeWriter writer, IProper writer.AppendLine("else"); writer.AppendLine("{"); writer.Indent(); - InstanceFactoryGenerator.GenerateInstanceCreation(writer, targetType, "instance"); + writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");"); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine($"var result = (({fullyQualifiedType})instance).{propertyAccess};"); @@ -1293,7 +1301,7 @@ private static void GeneratePropertyDataSourceFactory(CodeWriter writer, IProper writer.AppendLine("else"); writer.AppendLine("{"); writer.Indent(); - InstanceFactoryGenerator.GenerateInstanceCreation(writer, targetType, "instance"); + writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");"); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine($"var result = (({fullyQualifiedType})instance).{propertyAccess};"); @@ -1336,7 +1344,7 @@ private static void GeneratePropertyDataSourceFactory(CodeWriter writer, IProper writer.AppendLine("else"); writer.AppendLine("{"); writer.Indent(); - InstanceFactoryGenerator.GenerateInstanceCreation(writer, targetType, "instance"); + writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");"); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine($"var result = (({fullyQualifiedType})instance).{propertyAccess};"); diff --git a/TUnit.Core/Attributes/TestData/ClassDataSources.cs b/TUnit.Core/Attributes/TestData/ClassDataSources.cs index efad3790cd..0f21903fd1 100644 --- a/TUnit.Core/Attributes/TestData/ClassDataSources.cs +++ b/TUnit.Core/Attributes/TestData/ClassDataSources.cs @@ -1,7 +1,9 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.ExceptionServices; using TUnit.Core.Data; +using TUnit.Core.PropertyInjection; namespace TUnit.Core; @@ -87,7 +89,12 @@ private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemb try { - return Activator.CreateInstance(type)!; + var instance = Activator.CreateInstance(type)!; + + // Inject properties into the created instance + InjectPropertiesSync(instance, type, dataGeneratorMetadata, recursionDepth); + + return instance; } catch (TargetInvocationException targetInvocationException) { @@ -99,4 +106,197 @@ private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemb throw; } } + + /// + /// Injects properties into an instance synchronously. + /// Used when creating instances via ClassDataSource for nested data source dependencies. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Type is already annotated with DynamicallyAccessedMembers")] + [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling", Justification = "Fallback to reflection mode when source-gen not available")] + private static void InjectPropertiesSync( + object instance, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, + DataGeneratorMetadata dataGeneratorMetadata, + int recursionDepth) + { + // Get the injection plan for this type + var plan = PropertyInjectionPlanBuilder.Build(type); + if (!plan.HasProperties) + { + return; + } + + // Handle source-generated properties + foreach (var metadata in plan.SourceGeneratedProperties) + { + var dataSource = metadata.CreateDataSource(); + var propertyMetadata = CreatePropertyMetadata(type, metadata.PropertyName, metadata.PropertyType); + + var propertyDataGeneratorMetadata = DataGeneratorMetadataCreator.CreateForPropertyInjection( + propertyMetadata, + dataGeneratorMetadata.TestInformation, + dataSource, + testContext: null, + testClassInstance: instance, + events: new TestContextEvents(), + objectBag: new ConcurrentDictionary()); + + var value = ResolveDataSourceValueSync(dataSource, propertyDataGeneratorMetadata, recursionDepth + 1); + if (value != null) + { + metadata.SetProperty(instance, value); + } + } + + // Handle reflection-mode properties + foreach (var (property, dataSource) in plan.ReflectionProperties) + { + var propertyMetadata = CreatePropertyMetadataFromPropertyInfo(property); + + var propertyDataGeneratorMetadata = DataGeneratorMetadataCreator.CreateForPropertyInjection( + propertyMetadata, + dataGeneratorMetadata.TestInformation, + dataSource, + testContext: null, + testClassInstance: instance, + events: new TestContextEvents(), + objectBag: new ConcurrentDictionary()); + + var value = ResolveDataSourceValueSync(dataSource, propertyDataGeneratorMetadata, recursionDepth + 1); + if (value != null) + { + SetPropertyValue(property, instance, value); + } + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2067:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method", Justification = "Type is already annotated in caller")] + [UnconditionalSuppressMessage("Trimming", "IL2070:Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute'", Justification = "Type is already annotated in caller")] + private static PropertyMetadata CreatePropertyMetadata( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type containingType, + string propertyName, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] Type propertyType) + { + return new PropertyMetadata + { + Name = propertyName, + Type = propertyType, + IsStatic = false, + ClassMetadata = GetClassMetadataForType(containingType), + ContainingTypeMetadata = GetClassMetadataForType(containingType), + ReflectionInfo = containingType.GetProperty(propertyName)!, + Getter = parent => containingType.GetProperty(propertyName)?.GetValue(parent) + }; + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "PropertyInfo already obtained")] + [UnconditionalSuppressMessage("Trimming", "IL2072:'value' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method", Justification = "PropertyInfo already obtained with type annotations")] + private static PropertyMetadata CreatePropertyMetadataFromPropertyInfo(PropertyInfo property) + { + var containingType = property.DeclaringType!; + return new PropertyMetadata + { + Name = property.Name, + Type = property.PropertyType, + IsStatic = property.GetMethod?.IsStatic ?? false, + ClassMetadata = GetClassMetadataForType(containingType), + ContainingTypeMetadata = GetClassMetadataForType(containingType), + ReflectionInfo = property, + Getter = parent => property.GetValue(parent) + }; + } + + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Type is already annotated")] + [UnconditionalSuppressMessage("Trimming", "IL2070:Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute'", Justification = "Type is already annotated")] + [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method", Justification = "Type is already annotated")] + [UnconditionalSuppressMessage("Trimming", "IL2067:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method", Justification = "Type is already annotated")] + private static ClassMetadata GetClassMetadataForType(Type type) + { + return ClassMetadata.GetOrAdd(type.FullName ?? type.Name, () => + { + var constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance); + var constructor = constructors.FirstOrDefault(); + + var constructorParameters = constructor?.GetParameters().Select((p, i) => new ParameterMetadata(p.ParameterType) + { + Name = p.Name ?? $"param{i}", + TypeInfo = new ConcreteType(p.ParameterType), + ReflectionInfo = p + }).ToArray() ?? []; + + return new ClassMetadata + { + Type = type, + TypeInfo = new ConcreteType(type), + Name = type.Name, + Namespace = type.Namespace ?? string.Empty, + Assembly = AssemblyMetadata.GetOrAdd(type.Assembly.GetName().Name ?? type.Assembly.GetName().FullName ?? "Unknown", () => new AssemblyMetadata + { + Name = type.Assembly.GetName().Name ?? type.Assembly.GetName().FullName ?? "Unknown" + }), + Properties = [], + Parameters = constructorParameters, + Parent = type.DeclaringType != null ? GetClassMetadataForType(type.DeclaringType) : null + }; + }); + } + + /// + /// Resolves a data source value synchronously by running the async enumerable. + /// + private static object? ResolveDataSourceValueSync(IDataSourceAttribute dataSource, DataGeneratorMetadata metadata, int recursionDepth) + { + var dataRows = dataSource.GetDataRowsAsync(metadata); + + // Get the first value from the async enumerable synchronously + var enumerator = dataRows.GetAsyncEnumerator(); + try + { + if (enumerator.MoveNextAsync().AsTask().GetAwaiter().GetResult()) + { + var factory = enumerator.Current; + var args = factory().GetAwaiter().GetResult(); + if (args is { Length: > 0 }) + { + var value = args[0]; + + // Initialize the value if it implements IAsyncInitializer + ObjectInitializer.InitializeAsync(value).AsTask().GetAwaiter().GetResult(); + + return value; + } + } + } + finally + { + enumerator.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } + + return null; + } + + /// + /// Sets a property value, handling init-only properties via backing field if necessary. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "PropertyInfo already obtained")] + [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method", Justification = "PropertyInfo already obtained with type annotations")] + private static void SetPropertyValue(PropertyInfo property, object instance, object? value) + { + if (property.CanWrite && property.SetMethod != null) + { + property.SetValue(instance, value); + return; + } + + // Try to set via backing field for init-only properties + var backingFieldName = $"<{property.Name}>k__BackingField"; + var backingField = property.DeclaringType?.GetField( + backingFieldName, + BindingFlags.Instance | BindingFlags.NonPublic); + + if (backingField != null) + { + backingField.SetValue(instance, value); + } + } } diff --git a/TUnit.Core/Attributes/TestData/InstanceMethodDataSourceSourceAttribute.cs b/TUnit.Core/Attributes/TestData/InstanceMethodDataSourceSourceAttribute.cs index 28664cdc9f..9413df05a1 100644 --- a/TUnit.Core/Attributes/TestData/InstanceMethodDataSourceSourceAttribute.cs +++ b/TUnit.Core/Attributes/TestData/InstanceMethodDataSourceSourceAttribute.cs @@ -1,4 +1,25 @@ -namespace TUnit.Core; +using System.Diagnostics.CodeAnalysis; +namespace TUnit.Core; + +/// +/// A method data source attribute that requires an instance of the test class to be created first. +/// This implements IAccessesInstanceData which tells the engine to create a properly-initialized +/// instance before evaluating the data source. +/// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] -public class InstanceMethodDataSourceAttribute(string methodNameProvidingDataSource) : MethodDataSourceAttribute(methodNameProvidingDataSource), IAccessesInstanceData; +public class InstanceMethodDataSourceAttribute : MethodDataSourceAttribute, IAccessesInstanceData +{ + public InstanceMethodDataSourceAttribute(string methodNameProvidingDataSource) + : base(methodNameProvidingDataSource) + { + } + + public InstanceMethodDataSourceAttribute( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] + Type classProvidingDataSource, + string methodNameProvidingDataSource) + : base(classProvidingDataSource, methodNameProvidingDataSource) + { + } +} diff --git a/TUnit.Core/PropertyInjection/PropertyInjectionPlanBuilder.cs b/TUnit.Core/PropertyInjection/PropertyInjectionPlanBuilder.cs index a0cf5ee323..c0251ce158 100644 --- a/TUnit.Core/PropertyInjection/PropertyInjectionPlanBuilder.cs +++ b/TUnit.Core/PropertyInjection/PropertyInjectionPlanBuilder.cs @@ -102,13 +102,33 @@ public static PropertyInjectionPlan BuildReflectionPlan(Type type) /// /// Builds an injection plan based on the current execution mode. + /// Falls back to reflection when source-gen mode has no registered source for a type. + /// This handles generic types like ErrFixture<MyType> where the source generator + /// couldn't register a property source for the closed generic type. /// [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Source gen mode has its own path>")] public static PropertyInjectionPlan Build(Type type) { - return SourceRegistrar.IsEnabled - ? BuildSourceGeneratedPlan(type) - : BuildReflectionPlan(type); + if (!SourceRegistrar.IsEnabled) + { + return BuildReflectionPlan(type); + } + + // Try source-generated plan first + var plan = BuildSourceGeneratedPlan(type); + + // If no properties found in source-gen mode, fall back to reflection + // This handles generic types that couldn't be registered at compile time + if (!plan.HasProperties) + { + var reflectionPlan = BuildReflectionPlan(type); + if (reflectionPlan.HasProperties) + { + return reflectionPlan; + } + } + + return plan; } } diff --git a/TUnit.Engine/Building/TestBuilder.cs b/TUnit.Engine/Building/TestBuilder.cs index cc6da989b9..35993d71b6 100644 --- a/TUnit.Engine/Building/TestBuilder.cs +++ b/TUnit.Engine/Building/TestBuilder.cs @@ -250,6 +250,24 @@ public async Task> BuildTestsFromMetadataAsy // Non-generic class instanceForMethodDataSources = metadata.InstanceFactory([], classData); } + + // Initialize property data sources on the early instance so that + // method data sources can access fully-initialized properties. + // This is critical for scenarios like: + // [ClassDataSource>] public required ErrFixture Fixture { get; init; } + // public IEnumerable> TestExecutions => [() => Fixture.Value]; + // [MethodDataSource("TestExecutions")] [Test] public void MyTest(T value) { } + if (instanceForMethodDataSources != null) + { + var tempObjectBag = new ConcurrentDictionary(); + var tempEvents = new TestContextEvents(); + + await _propertyInjectionService.InjectPropertiesIntoObjectAsync( + instanceForMethodDataSources, + tempObjectBag, + metadata.MethodMetadata, + tempEvents); + } } catch (Exception ex) { @@ -1371,6 +1389,17 @@ public async IAsyncEnumerable BuildTestsStreamingAsync( { continue; // Skip if instance creation failed } + + // Initialize property data sources on the early instance so that + // method data sources can access fully-initialized properties. + var tempObjectBag = new ConcurrentDictionary(); + var tempEvents = new TestContextEvents(); + + await _propertyInjectionService.InjectPropertiesIntoObjectAsync( + instanceForMethodDataSources, + tempObjectBag, + metadata.MethodMetadata, + tempEvents); } // Stream through method data sources diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 556486d6e4..d3d12fecde 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -881,6 +881,7 @@ namespace public class InstanceMethodDataSourceAttribute : .MethodDataSourceAttribute, .IAccessesInstanceData { public InstanceMethodDataSourceAttribute(string methodNameProvidingDataSource) { } + public InstanceMethodDataSourceAttribute([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods | ..NonPublicMethods | ..PublicProperties)] classProvidingDataSource, string methodNameProvidingDataSource) { } } public class InvalidTestMetadataException : .TestBuilderException { diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt index 8bbca55ff3..3d1bb5b556 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -881,6 +881,7 @@ namespace public class InstanceMethodDataSourceAttribute : .MethodDataSourceAttribute, .IAccessesInstanceData { public InstanceMethodDataSourceAttribute(string methodNameProvidingDataSource) { } + public InstanceMethodDataSourceAttribute([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods | ..NonPublicMethods | ..PublicProperties)] classProvidingDataSource, string methodNameProvidingDataSource) { } } public class InvalidTestMetadataException : .TestBuilderException { diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt index e7945a9846..7f3054b2c2 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -881,6 +881,7 @@ namespace public class InstanceMethodDataSourceAttribute : .MethodDataSourceAttribute, .IAccessesInstanceData { public InstanceMethodDataSourceAttribute(string methodNameProvidingDataSource) { } + public InstanceMethodDataSourceAttribute([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods | ..NonPublicMethods | ..PublicProperties)] classProvidingDataSource, string methodNameProvidingDataSource) { } } public class InvalidTestMetadataException : .TestBuilderException { diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt index dd3923472e..ee302f89a4 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -858,6 +858,7 @@ namespace public class InstanceMethodDataSourceAttribute : .MethodDataSourceAttribute, .IAccessesInstanceData { public InstanceMethodDataSourceAttribute(string methodNameProvidingDataSource) { } + public InstanceMethodDataSourceAttribute( classProvidingDataSource, string methodNameProvidingDataSource) { } } public class InvalidTestMetadataException : .TestBuilderException { diff --git a/TUnit.TestProject/Bugs/3951/Tests.cs b/TUnit.TestProject/Bugs/3951/Tests.cs index 8527375a81..d54909778d 100644 --- a/TUnit.TestProject/Bugs/3951/Tests.cs +++ b/TUnit.TestProject/Bugs/3951/Tests.cs @@ -19,6 +19,7 @@ public class ErrContext: IAsyncInitializer, IAsyncDisposable public class ErrFixture : IAsyncDisposable, IAsyncInitializer { + [ClassDataSource(Shared = SharedType.PerClass)] public required ErrContext Fixture { get; set; } public ValueTask DisposeAsync() => default; public Task InitializeAsync() => Task.CompletedTask; @@ -43,6 +44,6 @@ public async Task MyTest(ErrContext context) [Test] public async Task MyTest2(MyType t) { - await Assert.That(t.GetType()).IsAssignableTo(); + await Assert.That(t).IsAssignableTo(); } }