diff --git a/TUnit.Core/Attributes/TestData/ClassDataSources.cs b/TUnit.Core/Attributes/TestData/ClassDataSources.cs index 0f21903fd1..d6fc652455 100644 --- a/TUnit.Core/Attributes/TestData/ClassDataSources.cs +++ b/TUnit.Core/Attributes/TestData/ClassDataSources.cs @@ -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; @@ -45,11 +43,11 @@ private string GetKey(int index, SharedType[] sharedTypes, string[] keys) { return sharedType switch { - SharedType.None => Create(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(), + 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() }; } @@ -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) { @@ -106,197 +88,4 @@ 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.Engine/Framework/TUnitServiceProvider.cs b/TUnit.Engine/Framework/TUnitServiceProvider.cs index dd6896971d..ed2543909d 100644 --- a/TUnit.Engine/Framework/TUnitServiceProvider.cs +++ b/TUnit.Engine/Framework/TUnitServiceProvider.cs @@ -188,7 +188,7 @@ public TUnitServiceProvider(IExtension extension, // Create test finder service after discovery service so it can use its cache TestFinder = Register(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( diff --git a/TUnit.Engine/Services/DataSourceInitializer.cs b/TUnit.Engine/Services/DataSourceInitializer.cs index ae69a2f52e..e69b0a385f 100644 --- a/TUnit.Engine/Services/DataSourceInitializer.cs +++ b/TUnit.Engine/Services/DataSourceInitializer.cs @@ -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)) { @@ -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)) { diff --git a/TUnit.Engine/Services/PropertyInitializationOrchestrator.cs b/TUnit.Engine/Services/PropertyInitializationOrchestrator.cs index 7462566749..2b7e3ab34b 100644 --- a/TUnit.Engine/Services/PropertyInitializationOrchestrator.cs +++ b/TUnit.Engine/Services/PropertyInitializationOrchestrator.cs @@ -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); diff --git a/TUnit.Engine/Services/PropertyInjectionService.cs b/TUnit.Engine/Services/PropertyInjectionService.cs index 9cdde83a2d..de10b2dfb7 100644 --- a/TUnit.Engine/Services/PropertyInjectionService.cs +++ b/TUnit.Engine/Services/PropertyInjectionService.cs @@ -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) { @@ -184,7 +186,7 @@ private async Task RecurseIntoNestedPropertiesAsync( } } } - else + else if (plan.ReflectionProperties.Length > 0) { foreach (var (property, _) in plan.ReflectionProperties) { diff --git a/TUnit.Engine/TestInitializer.cs b/TUnit.Engine/TestInitializer.cs index 2c8e0c7156..f9912932e4 100644 --- a/TUnit.Engine/TestInitializer.cs +++ b/TUnit.Engine/TestInitializer.cs @@ -10,14 +10,17 @@ internal class TestInitializer { private readonly EventReceiverOrchestrator _eventReceiverOrchestrator; private readonly PropertyInjectionService _propertyInjectionService; + private readonly DataSourceInitializer _dataSourceInitializer; private readonly ObjectTracker _objectTracker; public TestInitializer(EventReceiverOrchestrator eventReceiverOrchestrator, PropertyInjectionService propertyInjectionService, + DataSourceInitializer dataSourceInitializer, ObjectTracker objectTracker) { _eventReceiverOrchestrator = eventReceiverOrchestrator; _propertyInjectionService = propertyInjectionService; + _dataSourceInitializer = dataSourceInitializer; _objectTracker = objectTracker; } @@ -42,15 +45,27 @@ await _propertyInjectionService.InjectPropertiesIntoObjectAsync( private async Task InitializeTrackedObjects(TestContext testContext, CancellationToken cancellationToken) { // Initialize by level (deepest first), with objects at the same level in parallel + // Using DataSourceInitializer ensures property injection + nested objects + IAsyncInitializer var levels = testContext.TrackedObjects.Keys.OrderByDescending(level => level); foreach (var level in levels) { var objectsAtLevel = testContext.TrackedObjects[level]; - await Task.WhenAll(objectsAtLevel.Select(obj => ObjectInitializer.InitializeAsync(obj, cancellationToken).AsTask())); + await Task.WhenAll(objectsAtLevel.Select(obj => + _dataSourceInitializer.EnsureInitializedAsync( + obj, + testContext.StateBag.Items, + testContext.Metadata.TestDetails.MethodMetadata, + testContext.InternalEvents, + cancellationToken).AsTask())); } - // Finally, ensure the test class itself is initialized - await ObjectInitializer.InitializeAsync(testContext.Metadata.TestDetails.ClassInstance, cancellationToken); + // Finally, ensure the test class itself is initialized (property injection + IAsyncInitializer) + await _dataSourceInitializer.EnsureInitializedAsync( + testContext.Metadata.TestDetails.ClassInstance, + testContext.StateBag.Items, + testContext.Metadata.TestDetails.MethodMetadata, + testContext.InternalEvents, + cancellationToken); } } diff --git a/TUnit.TestProject/Bugs/3266/Tests.cs b/TUnit.TestProject/Bugs/3266/Tests.cs index f18ef81aed..25dadb37fa 100644 --- a/TUnit.TestProject/Bugs/3266/Tests.cs +++ b/TUnit.TestProject/Bugs/3266/Tests.cs @@ -1,17 +1,42 @@ +using System.Collections.Concurrent; using TUnit.Core; using TUnit.Core.Interfaces; using TUnit.TestProject.Attributes; namespace TUnit.TestProject.Bugs.Bug3266; +// Global tracker to verify initialization order across all components +public static class InitializationOrderTracker +{ + private static int _counter; + private static readonly ConcurrentDictionary InitOrder = new(); + + public static int RecordInitialization(string componentName) + { + var order = Interlocked.Increment(ref _counter); + InitOrder[componentName] = order; + return order; + } + + public static int GetOrder(string componentName) => InitOrder.TryGetValue(componentName, out var order) ? order : -1; + + public static void Reset() + { + _counter = 0; + InitOrder.Clear(); + } +} + // Mock test container - shared per test session public class PulsarTestContainer : IAsyncInitializer, IAsyncDisposable { public bool IsInitialized { get; private set; } public bool IsDisposed { get; private set; } + public int InitializationOrder { get; private set; } public Task InitializeAsync() { + InitializationOrder = InitializationOrderTracker.RecordInitialization(nameof(PulsarTestContainer)); IsInitialized = true; return Task.CompletedTask; } @@ -31,6 +56,7 @@ public class PulsarConnection : IAsyncInitializer, IAsyncDisposable public bool IsInitialized { get; private set; } public bool IsDisposed { get; private set; } + public int InitializationOrder { get; private set; } public Task InitializeAsync() { @@ -41,6 +67,7 @@ public Task InitializeAsync() "PulsarConnection.InitializeAsync() called before nested Container property was initialized!"); } + InitializationOrder = InitializationOrderTracker.RecordInitialization(nameof(PulsarConnection)); IsInitialized = true; return Task.CompletedTask; } @@ -60,6 +87,7 @@ public class WebAppFactory : IAsyncInitializer, IAsyncDisposable public bool IsInitialized { get; private set; } public bool IsDisposed { get; private set; } + public int InitializationOrder { get; private set; } public Task InitializeAsync() { @@ -70,6 +98,7 @@ public Task InitializeAsync() "WebAppFactory.InitializeAsync() called before nested Container property was initialized!"); } + InitializationOrder = InitializationOrderTracker.RecordInitialization(nameof(WebAppFactory)); IsInitialized = true; return Task.CompletedTask; } @@ -130,4 +159,280 @@ await Assert.That(Connection.Container) .IsSameReferenceAs(WebApp.Container) .Because("Both properties should share the same PulsarTestContainer instance"); } + + [Test] + public async Task InitializationOrderShouldBeDependencyFirst() + { + // PulsarTestContainer must be initialized before both PulsarConnection and WebAppFactory + await Assert.That(Connection.Container.InitializationOrder) + .IsLessThan(Connection.InitializationOrder) + .Because("Container should be initialized before PulsarConnection"); + + await Assert.That(WebApp.Container.InitializationOrder) + .IsLessThan(WebApp.InitializationOrder) + .Because("Container should be initialized before WebAppFactory"); + } +} + +#region Deep Nested Dependency Chain Tests + +// Level 1: Deepest shared dependency +public class DeepDependencyLevel1 : IAsyncInitializer, IAsyncDisposable +{ + public bool IsInitialized { get; private set; } + public int InitializationOrder { get; private set; } + + public Task InitializeAsync() + { + InitializationOrder = InitializationOrderTracker.RecordInitialization($"{nameof(DeepDependencyLevel1)}_{GetHashCode()}"); + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => default; +} + +// Level 2: Depends on Level 1 +public class DeepDependencyLevel2 : IAsyncInitializer, IAsyncDisposable +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required DeepDependencyLevel1 Level1 { get; init; } + + public bool IsInitialized { get; private set; } + public int InitializationOrder { get; private set; } + + public Task InitializeAsync() + { + if (!Level1.IsInitialized) + { + throw new InvalidOperationException("Level2 initialized before Level1!"); + } + InitializationOrder = InitializationOrderTracker.RecordInitialization($"{nameof(DeepDependencyLevel2)}_{GetHashCode()}"); + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => default; +} + +// Level 3: Depends on Level 2 +public class DeepDependencyLevel3 : IAsyncInitializer, IAsyncDisposable +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required DeepDependencyLevel2 Level2 { get; init; } + + public bool IsInitialized { get; private set; } + public int InitializationOrder { get; private set; } + + public Task InitializeAsync() + { + if (!Level2.IsInitialized) + { + throw new InvalidOperationException("Level3 initialized before Level2!"); + } + if (!Level2.Level1.IsInitialized) + { + throw new InvalidOperationException("Level3 initialized before Level1!"); + } + InitializationOrder = InitializationOrderTracker.RecordInitialization($"{nameof(DeepDependencyLevel3)}_{GetHashCode()}"); + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => default; +} + +// Level 4: Depends on Level 3 (deepest level in the chain) +public class DeepDependencyLevel4 : IAsyncInitializer, IAsyncDisposable +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required DeepDependencyLevel3 Level3 { get; init; } + + public bool IsInitialized { get; private set; } + public int InitializationOrder { get; private set; } + + public Task InitializeAsync() + { + if (!Level3.IsInitialized) + { + throw new InvalidOperationException("Level4 initialized before Level3!"); + } + InitializationOrder = InitializationOrderTracker.RecordInitialization($"{nameof(DeepDependencyLevel4)}_{GetHashCode()}"); + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => default; +} + +// Abstract base class in deep chain +public abstract class DeepAbstractBase +{ + [ClassDataSource(Shared = SharedType.None)] + public required DeepDependencyLevel4 DeepDependency { get; init; } +} + +// Concrete test for 4-level deep dependency chain +[EngineTest(ExpectedResult.Pass)] +[NotInParallel] +public class DeepDependencyChainTest : DeepAbstractBase +{ + [Test] + public async Task FourLevelDeepChainShouldInitializeInCorrectOrder() + { + // Verify all levels are initialized + await Assert.That(DeepDependency.Level3.Level2.Level1.IsInitialized).IsTrue(); + await Assert.That(DeepDependency.Level3.Level2.IsInitialized).IsTrue(); + await Assert.That(DeepDependency.Level3.IsInitialized).IsTrue(); + await Assert.That(DeepDependency.IsInitialized).IsTrue(); + + // Verify initialization order: Level1 < Level2 < Level3 < Level4 + await Assert.That(DeepDependency.Level3.Level2.Level1.InitializationOrder) + .IsLessThan(DeepDependency.Level3.Level2.InitializationOrder); + + await Assert.That(DeepDependency.Level3.Level2.InitializationOrder) + .IsLessThan(DeepDependency.Level3.InitializationOrder); + + await Assert.That(DeepDependency.Level3.InitializationOrder) + .IsLessThan(DeepDependency.InitializationOrder); + } +} + +#endregion + +#region MethodDataSource with Property Injection Tests + +// A fixture that uses property injection and provides data via MethodDataSource +public class FixtureWithMethodDataSource : IAsyncInitializer, IAsyncDisposable +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required PulsarTestContainer Container { get; init; } + + public bool IsInitialized { get; private set; } + + public Task InitializeAsync() + { + if (!Container.IsInitialized) + { + throw new InvalidOperationException("FixtureWithMethodDataSource initialized before its Container!"); + } + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => default; + + // This method provides test data and requires the fixture to be initialized + public IEnumerable> GetTestData() + { + if (!IsInitialized) + { + throw new InvalidOperationException("GetTestData called before fixture was initialized!"); + } + yield return () => "TestValue1"; + yield return () => "TestValue2"; + } +} + +// Test that combines MethodDataSource with ClassDataSource property injection +[EngineTest(ExpectedResult.Pass)] +[NotInParallel] +public class MethodDataSourceWithPropertyInjectionTest +{ + [ClassDataSource(Shared = SharedType.PerClass)] + public required FixtureWithMethodDataSource Fixture { get; init; } + + public IEnumerable> TestData => Fixture.GetTestData(); + + [Test] + [MethodDataSource(nameof(TestData))] + public async Task TestWithMethodDataFromInjectedFixture(string testValue) + { + // If we reach here, the initialization order was correct + await Assert.That(Fixture.IsInitialized).IsTrue(); + await Assert.That(Fixture.Container.IsInitialized).IsTrue(); + await Assert.That(testValue).IsNotNullOrEmpty(); + } +} + +#endregion + +#region Multiple Abstract Base Classes with Different Sharing Modes + +// Shared per class container +public class PerClassContainer : IAsyncInitializer, IAsyncDisposable +{ + public bool IsInitialized { get; private set; } + public string InstanceId { get; } = Guid.NewGuid().ToString(); + + public Task InitializeAsync() + { + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => default; +} + +// Dependency that uses PerClass shared container +public class PerClassDependency : IAsyncInitializer, IAsyncDisposable +{ + [ClassDataSource(Shared = SharedType.PerClass)] + public required PerClassContainer Container { get; init; } + + public bool IsInitialized { get; private set; } + + public Task InitializeAsync() + { + if (!Container.IsInitialized) + { + throw new InvalidOperationException("PerClassDependency initialized before Container!"); + } + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => default; +} + +// First abstract base with PerClass dependency +public abstract class AbstractWithPerClassDependency +{ + [ClassDataSource(Shared = SharedType.None)] + public required PerClassDependency PerClassDep { get; init; } +} + +// Second abstract layer with additional PerTestSession dependency +public abstract class AbstractWithMixedSharing : AbstractWithPerClassDependency +{ + [ClassDataSource(Shared = SharedType.None)] + public required PulsarConnection SessionDep { get; init; } +} + +// Test for mixed sharing modes in abstract hierarchy +[EngineTest(ExpectedResult.Pass)] +[NotInParallel] +public class MixedSharingModesInAbstractHierarchyTest : AbstractWithMixedSharing +{ + [Test] + public async Task AllDependenciesShouldBeProperlyInitialized() + { + // Verify PerClass chain + await Assert.That(PerClassDep.IsInitialized).IsTrue(); + await Assert.That(PerClassDep.Container.IsInitialized).IsTrue(); + + // Verify PerTestSession chain + await Assert.That(SessionDep.IsInitialized).IsTrue(); + await Assert.That(SessionDep.Container.IsInitialized).IsTrue(); + } + + [Test] + public async Task ContainersWithSameSharingModeAndTypeShouldBeShared() + { + // Both PulsarConnection and WebAppFactory should share the same PulsarTestContainer + // because they both use SharedType.PerTestSession for PulsarTestContainer + await Assert.That(SessionDep.Container).IsNotNull(); + } } + +#endregion