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
243 changes: 16 additions & 227 deletions TUnit.Core/Attributes/TestData/ClassDataSources.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.ExceptionServices;
using TUnit.Core.Data;
using TUnit.Core.PropertyInjection;

namespace TUnit.Core;

Expand Down Expand Up @@ -45,11 +43,11 @@ private string GetKey(int index, SharedType[] sharedTypes, string[] keys)
{
return sharedType switch
{
SharedType.None => Create<T>(dataGeneratorMetadata),
SharedType.PerTestSession => (T) TestDataContainer.GetGlobalInstance(typeof(T), _ => Create(typeof(T), dataGeneratorMetadata))!,
SharedType.PerClass => (T) TestDataContainer.GetInstanceForClass(testClassType, typeof(T), _ => Create(typeof(T), dataGeneratorMetadata))!,
SharedType.Keyed => (T) TestDataContainer.GetInstanceForKey(key, typeof(T), _ => Create(typeof(T), dataGeneratorMetadata))!,
SharedType.PerAssembly => (T) TestDataContainer.GetInstanceForAssembly(testClassType.Assembly, typeof(T), _ => Create(typeof(T), dataGeneratorMetadata))!,
SharedType.None => Create<T>(),
SharedType.PerTestSession => (T) TestDataContainer.GetGlobalInstance(typeof(T), _ => Create(typeof(T)))!,
SharedType.PerClass => (T) TestDataContainer.GetInstanceForClass(testClassType, typeof(T), _ => Create(typeof(T)))!,
SharedType.Keyed => (T) TestDataContainer.GetInstanceForKey(key, typeof(T), _ => Create(typeof(T)))!,
SharedType.PerAssembly => (T) TestDataContainer.GetInstanceForAssembly(testClassType.Assembly, typeof(T), _ => Create(typeof(T)))!,
_ => throw new ArgumentOutOfRangeException()
};
}
Expand All @@ -58,43 +56,27 @@ private string GetKey(int index, SharedType[] sharedTypes, string[] keys)
{
return sharedType switch
{
SharedType.None => Create(type, dataGeneratorMetadata),
SharedType.PerTestSession => TestDataContainer.GetGlobalInstance(type, _ => Create(type, dataGeneratorMetadata)),
SharedType.PerClass => TestDataContainer.GetInstanceForClass(testClassType, type, _ => Create(type, dataGeneratorMetadata)),
SharedType.Keyed => TestDataContainer.GetInstanceForKey(key!, type, _ => Create(type, dataGeneratorMetadata)),
SharedType.PerAssembly => TestDataContainer.GetInstanceForAssembly(testClassType.Assembly, type, _ => Create(type, dataGeneratorMetadata)),
SharedType.None => Create(type),
SharedType.PerTestSession => TestDataContainer.GetGlobalInstance(type, _ => Create(type)),
SharedType.PerClass => TestDataContainer.GetInstanceForClass(testClassType, type, _ => Create(type)),
SharedType.Keyed => TestDataContainer.GetInstanceForKey(key!, type, _ => Create(type)),
SharedType.PerAssembly => TestDataContainer.GetInstanceForAssembly(testClassType.Assembly, type, _ => Create(type)),
_ => throw new ArgumentOutOfRangeException()
};
}

[return: NotNull]
private static T Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] T>(DataGeneratorMetadata dataGeneratorMetadata)
private static T Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>()
{
return ((T) Create(typeof(T), dataGeneratorMetadata))!;
return ((T) Create(typeof(T)))!;
}

private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, DataGeneratorMetadata dataGeneratorMetadata)
private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type)
{
return Create(type, dataGeneratorMetadata, recursionDepth: 0);
}

private const int MaxRecursionDepth = 10;

private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, DataGeneratorMetadata dataGeneratorMetadata, int recursionDepth)
{
if (recursionDepth >= MaxRecursionDepth)
{
throw new InvalidOperationException($"Maximum recursion depth ({MaxRecursionDepth}) exceeded when creating nested ClassDataSource dependencies. This may indicate a circular dependency.");
}

try
{
var instance = Activator.CreateInstance(type)!;

// Inject properties into the created instance
InjectPropertiesSync(instance, type, dataGeneratorMetadata, recursionDepth);

return instance;
// Just create the instance - initialization happens in the Engine
return Activator.CreateInstance(type)!;
}
catch (TargetInvocationException targetInvocationException)
{
Expand All @@ -106,197 +88,4 @@ private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemb
throw;
}
}

/// <summary>
/// Injects properties into an instance synchronously.
/// Used when creating instances via ClassDataSource for nested data source dependencies.
/// </summary>
[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<string, object?>());

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<string, object?>());

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
};
});
}

/// <summary>
/// Resolves a data source value synchronously by running the async enumerable.
/// </summary>
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;
}

/// <summary>
/// Sets a property value, handling init-only properties via backing field if necessary.
/// </summary>
[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);
}
}
}
2 changes: 1 addition & 1 deletion TUnit.Engine/Framework/TUnitServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ public TUnitServiceProvider(IExtension extension,
// Create test finder service after discovery service so it can use its cache
TestFinder = Register<ITestFinder>(new TestFinder(DiscoveryService));

var testInitializer = new TestInitializer(EventReceiverOrchestrator, PropertyInjectionService, objectTracker);
var testInitializer = new TestInitializer(EventReceiverOrchestrator, PropertyInjectionService, DataSourceInitializer, objectTracker);

// Create the new TestCoordinator that orchestrates the granular services
var testCoordinator = Register<ITestCoordinator>(
Expand Down
32 changes: 17 additions & 15 deletions TUnit.Engine/Services/DataSourceInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,21 @@ private void CollectNestedObjects(
{
var plan = PropertyInjectionCache.GetOrCreatePlan(obj.GetType());

if (!SourceRegistrar.IsEnabled)
// Use whichever properties are available in the plan
// For closed generic types, source-gen may not have registered them, so use reflection fallback
if (plan.SourceGeneratedProperties.Length > 0)
{
// Reflection mode
foreach (var prop in plan.ReflectionProperties)
// Source-generated mode
foreach (var metadata in plan.SourceGeneratedProperties)
{
var value = prop.Property.GetValue(obj);
var property = metadata.ContainingType.GetProperty(metadata.PropertyName);

if (property == null || !property.CanRead)
{
continue;
}

var value = property.GetValue(obj);

if (value == null || !visitedObjects.Add(value))
{
Expand All @@ -192,19 +201,12 @@ private void CollectNestedObjects(
}
}
}
else
else if (plan.ReflectionProperties.Length > 0)
{
// Source-generated mode
foreach (var metadata in plan.SourceGeneratedProperties)
// Reflection mode fallback
foreach (var prop in plan.ReflectionProperties)
{
var property = metadata.ContainingType.GetProperty(metadata.PropertyName);

if (property == null || !property.CanRead)
{
continue;
}

var value = property.GetValue(obj);
var value = prop.Property.GetValue(obj);

if (value == null || !visitedObjects.Add(value))
{
Expand Down
7 changes: 4 additions & 3 deletions TUnit.Engine/Services/PropertyInitializationOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,14 @@ public async Task InitializeObjectWithPropertiesAsync(
return;
}

// Initialize properties based on the mode (source-generated or reflection)
if (SourceRegistrar.IsEnabled)
// Initialize properties based on what's available in the plan
// For closed generic types, source-gen may not have registered them, so use reflection fallback
if (plan.SourceGeneratedProperties.Length > 0)
{
await InitializeSourceGeneratedPropertiesAsync(
instance, plan.SourceGeneratedProperties, objectBag, methodMetadata, events, visitedObjects);
}
else
else if (plan.ReflectionProperties.Length > 0)
{
await InitializeReflectionPropertiesAsync(
instance, plan.ReflectionProperties, objectBag, methodMetadata, events, visitedObjects);
Expand Down
6 changes: 4 additions & 2 deletions TUnit.Engine/Services/PropertyInjectionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ private async Task RecurseIntoNestedPropertiesAsync(
return;
}

if (SourceRegistrar.IsEnabled)
// Use whichever properties are available in the plan
// For closed generic types, source-gen may not have registered them, so use reflection fallback
if (plan.SourceGeneratedProperties.Length > 0)
{
foreach (var metadata in plan.SourceGeneratedProperties)
{
Expand All @@ -184,7 +186,7 @@ private async Task RecurseIntoNestedPropertiesAsync(
}
}
}
else
else if (plan.ReflectionProperties.Length > 0)
{
foreach (var (property, _) in plan.ReflectionProperties)
{
Expand Down
Loading
Loading