From 40112eea756c5f2d3965bdd5621602802875ad4a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Fri, 19 Sep 2025 12:45:22 +0100 Subject: [PATCH 1/4] feat(tests): add CallEventReceiverTests and improve event receiver error handling --- TUnit.Core/Data/ThreadSafeDictionary.cs | 13 +- .../Services/EventReceiverOrchestrator.cs | 117 +++++------------- TUnit.Engine/Services/HookExecutor.cs | 52 -------- TUnit.TestProject/CallEventReceiverTests.cs | 44 +++++++ 4 files changed, 83 insertions(+), 143 deletions(-) create mode 100644 TUnit.TestProject/CallEventReceiverTests.cs diff --git a/TUnit.Core/Data/ThreadSafeDictionary.cs b/TUnit.Core/Data/ThreadSafeDictionary.cs index 85db43b39e..9450888184 100644 --- a/TUnit.Core/Data/ThreadSafeDictionary.cs +++ b/TUnit.Core/Data/ThreadSafeDictionary.cs @@ -7,8 +7,8 @@ namespace TUnit.Core.Data; #if !DEBUG [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] #endif -public class ThreadSafeDictionary +public class ThreadSafeDictionary where TKey : notnull { // Using Lazy ensures factory functions are only executed once per key, @@ -24,8 +24,9 @@ public TValue GetOrAdd(TKey key, Func func) // The Lazy wrapper ensures the factory function is only executed once, // even if multiple threads race to add the same key // We use ExecutionAndPublication mode for thread safety - var lazy = _innerDictionary.GetOrAdd(key, + var lazy = _innerDictionary.GetOrAdd(key, k => new Lazy(() => func(k), LazyThreadSafetyMode.ExecutionAndPublication)); + return lazy.Value; } @@ -36,7 +37,7 @@ public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value) value = lazy.Value!; return true; } - + value = default!; return false; } @@ -51,7 +52,7 @@ public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value) return default(TValue?); } - public TValue this[TKey key] => _innerDictionary.TryGetValue(key, out var lazy) - ? lazy.Value + public TValue this[TKey key] => _innerDictionary.TryGetValue(key, out var lazy) + ? lazy.Value : throw new KeyNotFoundException($"Key '{key}' not found in dictionary"); } diff --git a/TUnit.Engine/Services/EventReceiverOrchestrator.cs b/TUnit.Engine/Services/EventReceiverOrchestrator.cs index 8c6d3ad9d3..6c8ffd6510 100644 --- a/TUnit.Engine/Services/EventReceiverOrchestrator.cs +++ b/TUnit.Engine/Services/EventReceiverOrchestrator.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; using TUnit.Core; using TUnit.Core.Data; +using TUnit.Core.Helpers; using TUnit.Core.Interfaces; using TUnit.Engine.Events; using TUnit.Engine.Extensions; @@ -22,8 +23,8 @@ internal sealed class EventReceiverOrchestrator : IDisposable private ThreadSafeDictionary _firstTestInSessionTasks = new(); // Track remaining test counts for "last" events - private readonly ConcurrentDictionary _assemblyTestCounts = new(); - private readonly ConcurrentDictionary _classTestCounts = new(); + private readonly ThreadSafeDictionary _assemblyTestCounts = new(); + private readonly ThreadSafeDictionary _classTestCounts = new(); private int _sessionTestCount; public EventReceiverOrchestrator(TUnitFrameworkLogger logger) @@ -40,14 +41,7 @@ public async ValueTask InitializeAllEligibleObjectsAsync(TestContext context, Ca foreach (var obj in eligibleObjects) { - try - { - await ObjectInitializer.InitializeAsync(obj, cancellationToken); - } - catch (Exception ex) - { - await _logger.LogErrorAsync($"Error initializing object of type {obj.GetType().Name}: {ex.Message}"); - } + await ObjectInitializer.InitializeAsync(obj, cancellationToken); } } @@ -85,14 +79,7 @@ private async ValueTask InvokeTestStartEventReceiversCore(TestContext context, C // Sequential for small counts foreach (var receiver in filteredReceivers) { - try - { - await receiver.OnTestStart(context); - } - catch (Exception ex) - { - await _logger.LogErrorAsync($"Error in test start event receiver: {ex.Message}"); - } + await receiver.OnTestStart(context); } } } @@ -154,14 +141,7 @@ private async ValueTask InvokeTestSkippedEventReceiversCore(TestContext context, foreach (var receiver in filteredReceivers) { - try - { - await receiver.OnTestSkipped(context); - } - catch (Exception ex) - { - await _logger.LogErrorAsync($"Error in test skipped event receiver: {ex.Message}"); - } + await receiver.OnTestSkipped(context); } } @@ -177,14 +157,7 @@ public async ValueTask InvokeTestDiscoveryEventReceiversAsync(TestContext contex foreach (var receiver in filteredReceivers.OrderBy(r => r.Order)) { - try - { - await receiver.OnTestDiscovered(discoveredContext); - } - catch (Exception ex) - { - await _logger.LogErrorAsync($"Error in test discovery event receiver: {ex.Message}"); - } + await receiver.OnTestDiscovered(discoveredContext); } } @@ -201,14 +174,7 @@ public async ValueTask InvokeHookRegistrationEventReceiversAsync(HookRegisteredC foreach (var receiver in filteredReceivers.OrderBy(r => r.Order)) { - try - { - await receiver.OnHookRegistered(hookContext); - } - catch (Exception ex) - { - await _logger.LogErrorAsync($"Error in hook registration event receiver: {ex.Message}"); - } + await receiver.OnHookRegistered(hookContext); } // Apply the timeout from the context back to the hook method @@ -246,14 +212,7 @@ private async Task InvokeFirstTestInSessionEventReceiversCoreAsync( foreach (var receiver in receivers) { - try - { - await receiver.OnFirstTestInTestSession(sessionContext, context); - } - catch (Exception ex) - { - await _logger.LogErrorAsync($"Error in first test in session event receiver: {ex.Message}"); - } + await receiver.OnFirstTestInTestSession(sessionContext, context); } } @@ -270,9 +229,8 @@ public async ValueTask InvokeFirstTestInAssemblyEventReceiversAsync( var assemblyName = assemblyContext.Assembly.GetName().FullName ?? ""; // Use GetOrAdd to ensure exactly one task is created per assembly and all tests await it - var task = _firstTestInAssemblyTasks.GetOrAdd(assemblyName, + await _firstTestInAssemblyTasks.GetOrAdd(assemblyName, _ => InvokeFirstTestInAssemblyEventReceiversCoreAsync(context, assemblyContext, cancellationToken)); - await task; } private async Task InvokeFirstTestInAssemblyEventReceiversCoreAsync( @@ -284,14 +242,7 @@ private async Task InvokeFirstTestInAssemblyEventReceiversCoreAsync( foreach (var receiver in receivers) { - try - { - await receiver.OnFirstTestInAssembly(assemblyContext, context); - } - catch (Exception ex) - { - await _logger.LogErrorAsync($"Error in first test in assembly event receiver: {ex.Message}"); - } + await receiver.OnFirstTestInAssembly(assemblyContext, context); } } @@ -322,14 +273,7 @@ private async Task InvokeFirstTestInClassEventReceiversCoreAsync( foreach (var receiver in receivers) { - try - { - await receiver.OnFirstTestInClass(classContext, context); - } - catch (Exception ex) - { - await _logger.LogErrorAsync($"Error in first test in class event receiver: {ex.Message}"); - } + await receiver.OnFirstTestInClass(classContext, context); } } @@ -399,7 +343,10 @@ public async ValueTask InvokeLastTestInAssemblyEventReceiversAsync( } var assemblyName = assemblyContext.Assembly.GetName().FullName ?? ""; - if (_assemblyTestCounts.AddOrUpdate(assemblyName, 0, (_, count) => count - 1) == 0) + + var assemblyCount = _assemblyTestCounts.GetOrAdd(assemblyName, _ => new Counter()).Decrement(); + + if (assemblyCount == 0) { await InvokeLastTestInAssemblyEventReceiversCore(context, assemblyContext, cancellationToken); } @@ -437,7 +384,10 @@ public async ValueTask InvokeLastTestInClassEventReceiversAsync( } var classType = classContext.ClassType; - if (_classTestCounts.AddOrUpdate(classType, 0, (_, count) => count - 1) == 0) + + var classCount = _classTestCounts.GetOrAdd(classType, _ => new Counter()).Decrement(); + + if (classCount == 0) { await InvokeLastTestInClassEventReceiversCore(context, classContext, cancellationToken); } @@ -476,19 +426,23 @@ public void InitializeTestCounts(IEnumerable allTestContexts) _firstTestInClassTasks = new ThreadSafeDictionary(); _firstTestInSessionTasks = new ThreadSafeDictionary(); - foreach (var group in contexts.Where(c => c.ClassContext != null).GroupBy(c => c.ClassContext!.AssemblyContext.Assembly.GetName().FullName)) + foreach (var group in contexts.GroupBy(c => c.ClassContext.AssemblyContext.Assembly.GetName().FullName)) { - if (group.Key != null) + var counter = _assemblyTestCounts.GetOrAdd(group.Key, _ => new Counter()); + + for (var i = 0; i < group.Count(); i++) { - _assemblyTestCounts[group.Key] = group.Count(); + counter.Increment(); } } - foreach (var group in contexts.Where(c => c.ClassContext != null).GroupBy(c => c.ClassContext!.ClassType)) + foreach (var group in contexts.GroupBy(c => c.ClassContext.ClassType)) { - if (group.Key != null) + var counter = _classTestCounts.GetOrAdd(group.Key, _ => new Counter()); + + for (var i = 0; i < group.Count(); i++) { - _classTestCounts[group.Key] = group.Count(); + counter.Increment(); } } } @@ -517,18 +471,11 @@ private async Task InvokeReceiverAsync( Func invoker, CancellationToken cancellationToken) where T : IEventReceiver { - try - { - await invoker(receiver); - } - catch (Exception ex) - { - await _logger.LogErrorAsync($"Event receiver {receiver.GetType().Name} threw exception: {ex.Message}"); - } + await invoker(receiver); } public void Dispose() { - _registry?.Dispose(); + _registry.Dispose(); } } diff --git a/TUnit.Engine/Services/HookExecutor.cs b/TUnit.Engine/Services/HookExecutor.cs index 4577ce9e6f..b3c4be4e99 100644 --- a/TUnit.Engine/Services/HookExecutor.cs +++ b/TUnit.Engine/Services/HookExecutor.cs @@ -48,22 +48,6 @@ public async Task ExecuteBeforeTestSessionHooksAsync(CancellationToken cancellat } } - /// - /// Execute before test session hooks AND first test in session event receivers for a specific test context. - /// This consolidates both lifecycle mechanisms into a single call. - /// - public async Task ExecuteBeforeTestSessionHooksAsync(TestContext testContext, CancellationToken cancellationToken) - { - // Execute regular before session hooks - await ExecuteBeforeTestSessionHooksAsync(cancellationToken).ConfigureAwait(false); - - // Also execute first test in session event receivers (these run only once via internal task coordination) - await _eventReceiverOrchestrator.InvokeFirstTestInSessionEventReceiversAsync( - testContext, - testContext.ClassContext.AssemblyContext.TestSessionContext, - cancellationToken).ConfigureAwait(false); - } - public async Task ExecuteAfterTestSessionHooksAsync(CancellationToken cancellationToken) { var hooks = await _hookCollectionService.CollectAfterTestSessionHooksAsync().ConfigureAwait(false); @@ -100,24 +84,6 @@ public async Task ExecuteBeforeAssemblyHooksAsync(Assembly assembly, Cancellatio } } - /// - /// Execute before assembly hooks AND first test in assembly event receivers for a specific test context. - /// This consolidates both lifecycle mechanisms into a single call. - /// - public async Task ExecuteBeforeAssemblyHooksAsync(TestContext testContext, CancellationToken cancellationToken) - { - var assembly = testContext.TestDetails.ClassType.Assembly; - - // Execute regular before assembly hooks - await ExecuteBeforeAssemblyHooksAsync(assembly, cancellationToken).ConfigureAwait(false); - - // Also execute first test in assembly event receivers - await _eventReceiverOrchestrator.InvokeFirstTestInAssemblyEventReceiversAsync( - testContext, - testContext.ClassContext.AssemblyContext, - cancellationToken).ConfigureAwait(false); - } - public async Task ExecuteAfterAssemblyHooksAsync(Assembly assembly, CancellationToken cancellationToken) { var hooks = await _hookCollectionService.CollectAfterAssemblyHooksAsync(assembly).ConfigureAwait(false); @@ -156,24 +122,6 @@ public async Task ExecuteBeforeClassHooksAsync( } } - /// - /// Execute before class hooks AND first test in class event receivers for a specific test context. - /// This consolidates both lifecycle mechanisms into a single call. - /// - public async Task ExecuteBeforeClassHooksAsync(TestContext testContext, CancellationToken cancellationToken) - { - var testClass = testContext.TestDetails.ClassType; - - // Execute regular before class hooks - await ExecuteBeforeClassHooksAsync(testClass, cancellationToken).ConfigureAwait(false); - - // Also execute first test in class event receivers - await _eventReceiverOrchestrator.InvokeFirstTestInClassEventReceiversAsync( - testContext, - testContext.ClassContext, - cancellationToken).ConfigureAwait(false); - } - public async Task ExecuteAfterClassHooksAsync( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)] Type testClass, CancellationToken cancellationToken) diff --git a/TUnit.TestProject/CallEventReceiverTests.cs b/TUnit.TestProject/CallEventReceiverTests.cs new file mode 100644 index 0000000000..3122d32d2c --- /dev/null +++ b/TUnit.TestProject/CallEventReceiverTests.cs @@ -0,0 +1,44 @@ +using TUnit.Core.Interfaces; + +namespace TUnit.TestProject; + +public class CallEventReceiverTests : IFirstTestInAssemblyEventReceiver +{ + private static int _beforeAssemblyInvoked; + private static int _firstTestInAssemblyInvoked; + + [Test] + public async Task Test1() + { + var result = true; + await Assert.That(result).IsTrue(); + } + + [Test] + public async Task Test2() + { + var result = true; + await Assert.That(result).IsTrue(); + } + + [Before(Assembly)] + public static async Task Before_assembly() + { + Interlocked.Increment(ref _beforeAssemblyInvoked); + + Console.WriteLine($@"Before Assembly = {_beforeAssemblyInvoked}"); + + await Assert.That(_beforeAssemblyInvoked).IsEqualTo(1); + } + + public async ValueTask OnFirstTestInAssembly(AssemblyHookContext context, TestContext testContext) + { + Interlocked.Increment(ref _firstTestInAssemblyInvoked); + + Console.WriteLine($@"OnFirstTestInAssembly = {_firstTestInAssemblyInvoked}"); + + await Assert.That(_firstTestInAssemblyInvoked).IsEqualTo(1); + } + + public int Order => 0; +} From eabbae346578b06fa9ef2c08ed7715972ffc68f1 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Fri, 19 Sep 2025 13:07:07 +0100 Subject: [PATCH 2/4] Fix items being registered for event notifications multiple times --- .../Services/EventReceiverOrchestrator.cs | 57 +++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/TUnit.Engine/Services/EventReceiverOrchestrator.cs b/TUnit.Engine/Services/EventReceiverOrchestrator.cs index 6c8ffd6510..c7dde5dc8e 100644 --- a/TUnit.Engine/Services/EventReceiverOrchestrator.cs +++ b/TUnit.Engine/Services/EventReceiverOrchestrator.cs @@ -26,6 +26,12 @@ internal sealed class EventReceiverOrchestrator : IDisposable private readonly ThreadSafeDictionary _assemblyTestCounts = new(); private readonly ThreadSafeDictionary _classTestCounts = new(); private int _sessionTestCount; + + // Track which objects have already been initialized to avoid duplicates + private readonly HashSet _initializedObjects = new(); + + // Track registered First event receiver types to avoid duplicate registrations + private readonly HashSet _registeredFirstEventReceiverTypes = new(); public EventReceiverOrchestrator(TUnitFrameworkLogger logger) { @@ -36,12 +42,52 @@ public async ValueTask InitializeAllEligibleObjectsAsync(TestContext context, Ca { var eligibleObjects = context.GetEligibleEventObjects().ToArray(); - // Register all event receivers for fast lookup - _registry.RegisterReceivers(eligibleObjects); - + // Only initialize and register objects that haven't been processed yet + var newObjects = new List(); + var objectsToRegister = new List(); + foreach (var obj in eligibleObjects) { - await ObjectInitializer.InitializeAsync(obj, cancellationToken); + if (_initializedObjects.Add(obj)) // Add returns false if already present + { + newObjects.Add(obj); + + // For First event receivers, only register one instance per type + var objType = obj.GetType(); + bool isFirstEventReceiver = obj is IFirstTestInTestSessionEventReceiver || + obj is IFirstTestInAssemblyEventReceiver || + obj is IFirstTestInClassEventReceiver; + + if (isFirstEventReceiver) + { + if (_registeredFirstEventReceiverTypes.Add(objType)) + { + // First instance of this type, register it + objectsToRegister.Add(obj); + } + // else: Skip registration, we already have an instance of this type + } + else + { + // Not a First event receiver, register normally + objectsToRegister.Add(obj); + } + } + } + + if (objectsToRegister.Count > 0) + { + // Register only the objects that should be registered + _registry.RegisterReceivers(objectsToRegister); + } + + if (newObjects.Count > 0) + { + // Initialize all new objects (even if not registered) + foreach (var obj in newObjects) + { + await ObjectInitializer.InitializeAsync(obj, cancellationToken); + } } } @@ -229,8 +275,9 @@ public async ValueTask InvokeFirstTestInAssemblyEventReceiversAsync( var assemblyName = assemblyContext.Assembly.GetName().FullName ?? ""; // Use GetOrAdd to ensure exactly one task is created per assembly and all tests await it - await _firstTestInAssemblyTasks.GetOrAdd(assemblyName, + var task = _firstTestInAssemblyTasks.GetOrAdd(assemblyName, _ => InvokeFirstTestInAssemblyEventReceiversCoreAsync(context, assemblyContext, cancellationToken)); + await task; } private async Task InvokeFirstTestInAssemblyEventReceiversCoreAsync( From e82fc7f71cbfa24cec1d7c86b804b7f93f51beac Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:34:46 +0100 Subject: [PATCH 3/4] feat(property-injection): implement property initialization strategies and refactor injection logic --- .../AsyncDataSourceGeneratorAttribute.cs | 52 +- ...typedDataSourceSourceGeneratorAttribute.cs | 13 +- .../Initialization/TestObjectInitializer.cs | 220 ++++++ .../PropertyInjection/ClassMetadataHelper.cs | 57 ++ .../Initialization/PropertyDataResolver.cs | 133 ++++ .../PropertyInitializationContext.cs | 89 +++ .../PropertyInitializationOrchestrator.cs | 166 +++++ .../PropertyInitializationPipeline.cs | 154 ++++ .../Initialization/PropertyTrackingService.cs | 144 ++++ .../IPropertyInitializationStrategy.cs | 20 + .../Strategies/NestedPropertyStrategy.cs | 160 ++++ .../Strategies/ReflectionPropertyStrategy.cs | 75 ++ .../SourceGeneratedPropertyStrategy.cs | 73 ++ .../PropertyInjection/PropertyHelper.cs | 31 + .../PropertyInjectionCache.cs | 52 ++ .../PropertyInjectionPlanBuilder.cs | 87 +++ .../PropertySetterFactory.cs | 108 +++ .../PropertyValueProcessor.cs | 94 +++ .../PropertyInjection/TupleValueResolver.cs | 64 ++ TUnit.Core/PropertyInjectionService.cs | 697 +----------------- .../Collectors/AotTestDataCollector.cs | 2 +- .../Discovery/ReflectionTestDataCollector.cs | 4 +- .../Services/TestArgumentTrackingService.cs | 39 +- TUnit.Engine/Services/TestRegistry.cs | 2 +- TUnit.Engine/TestInitializer.cs | 9 +- 25 files changed, 1832 insertions(+), 713 deletions(-) create mode 100644 TUnit.Core/Initialization/TestObjectInitializer.cs create mode 100644 TUnit.Core/PropertyInjection/ClassMetadataHelper.cs create mode 100644 TUnit.Core/PropertyInjection/Initialization/PropertyDataResolver.cs create mode 100644 TUnit.Core/PropertyInjection/Initialization/PropertyInitializationContext.cs create mode 100644 TUnit.Core/PropertyInjection/Initialization/PropertyInitializationOrchestrator.cs create mode 100644 TUnit.Core/PropertyInjection/Initialization/PropertyInitializationPipeline.cs create mode 100644 TUnit.Core/PropertyInjection/Initialization/PropertyTrackingService.cs create mode 100644 TUnit.Core/PropertyInjection/Initialization/Strategies/IPropertyInitializationStrategy.cs create mode 100644 TUnit.Core/PropertyInjection/Initialization/Strategies/NestedPropertyStrategy.cs create mode 100644 TUnit.Core/PropertyInjection/Initialization/Strategies/ReflectionPropertyStrategy.cs create mode 100644 TUnit.Core/PropertyInjection/Initialization/Strategies/SourceGeneratedPropertyStrategy.cs create mode 100644 TUnit.Core/PropertyInjection/PropertyHelper.cs create mode 100644 TUnit.Core/PropertyInjection/PropertyInjectionCache.cs create mode 100644 TUnit.Core/PropertyInjection/PropertyInjectionPlanBuilder.cs create mode 100644 TUnit.Core/PropertyInjection/PropertySetterFactory.cs create mode 100644 TUnit.Core/PropertyInjection/PropertyValueProcessor.cs create mode 100644 TUnit.Core/PropertyInjection/TupleValueResolver.cs diff --git a/TUnit.Core/Attributes/TestData/AsyncDataSourceGeneratorAttribute.cs b/TUnit.Core/Attributes/TestData/AsyncDataSourceGeneratorAttribute.cs index 3826979471..80b3ac163c 100644 --- a/TUnit.Core/Attributes/TestData/AsyncDataSourceGeneratorAttribute.cs +++ b/TUnit.Core/Attributes/TestData/AsyncDataSourceGeneratorAttribute.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using TUnit.Core.Extensions; +using TUnit.Core.Initialization; namespace TUnit.Core; @@ -8,19 +9,22 @@ public abstract class AsyncDataSourceGeneratorAttribute<[DynamicallyAccessedMemb { protected abstract IAsyncEnumerable>> GenerateDataSourcesAsync(DataGeneratorMetadata dataGeneratorMetadata); - public override async IAsyncEnumerable>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata) + public sealed override async IAsyncEnumerable>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata) { - // Inject properties into the data source attribute itself if we have context - // This is needed for custom data sources that have their own data source properties + // Use centralized TestObjectInitializer for all initialization + // This handles both property injection and object initialization if (dataGeneratorMetadata is { TestInformation: not null }) { - await PropertyInjectionService.InjectPropertiesIntoObjectAsync(this, + await TestObjectInitializer.InitializeAsync(this, dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag, dataGeneratorMetadata.TestInformation, dataGeneratorMetadata.TestBuilderContext.Current.Events); } - - await ObjectInitializer.InitializeAsync(this); + else + { + // Fallback if no context available + await TestObjectInitializer.InitializeAsync(this, TestContext.Current); + } await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata)) { @@ -38,18 +42,20 @@ public abstract class AsyncDataSourceGeneratorAttribute< { protected abstract IAsyncEnumerable>> GenerateDataSourcesAsync(DataGeneratorMetadata dataGeneratorMetadata); - public override async IAsyncEnumerable>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata) + public sealed override async IAsyncEnumerable>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata) { // Inject properties into the data source attribute itself if we have context if (dataGeneratorMetadata is { TestInformation: not null }) { - await PropertyInjectionService.InjectPropertiesIntoObjectAsync(this, + await TestObjectInitializer.InitializeAsync(this, dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag, dataGeneratorMetadata.TestInformation, dataGeneratorMetadata.TestBuilderContext.Current.Events); } - - await ObjectInitializer.InitializeAsync(this); + else + { + await TestObjectInitializer.InitializeAsync(this, TestContext.Current); + } await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata)) { @@ -69,18 +75,20 @@ public abstract class AsyncDataSourceGeneratorAttribute< { protected abstract IAsyncEnumerable>> GenerateDataSourcesAsync(DataGeneratorMetadata dataGeneratorMetadata); - public override async IAsyncEnumerable>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata) + public sealed override async IAsyncEnumerable>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata) { // Inject properties into the data source attribute itself if we have context if (dataGeneratorMetadata is { TestInformation: not null }) { - await PropertyInjectionService.InjectPropertiesIntoObjectAsync(this, + await TestObjectInitializer.InitializeAsync(this, dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag, dataGeneratorMetadata.TestInformation, dataGeneratorMetadata.TestBuilderContext.Current.Events); } - - await ObjectInitializer.InitializeAsync(this); + else + { + await TestObjectInitializer.InitializeAsync(this, TestContext.Current); + } await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata)) { @@ -107,13 +115,15 @@ public abstract class AsyncDataSourceGeneratorAttribute< // Inject properties into the data source attribute itself if we have context if (dataGeneratorMetadata is { TestInformation: not null }) { - await PropertyInjectionService.InjectPropertiesIntoObjectAsync(this, + await TestObjectInitializer.InitializeAsync(this, dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag, dataGeneratorMetadata.TestInformation, dataGeneratorMetadata.TestBuilderContext.Current.Events); } - - await ObjectInitializer.InitializeAsync(this); + else + { + await TestObjectInitializer.InitializeAsync(this, TestContext.Current); + } await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata)) { @@ -142,13 +152,15 @@ public abstract class AsyncDataSourceGeneratorAttribute< // Inject properties into the data source attribute itself if we have context if (dataGeneratorMetadata is { TestInformation: not null }) { - await PropertyInjectionService.InjectPropertiesIntoObjectAsync(this, + await TestObjectInitializer.InitializeAsync(this, dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag, dataGeneratorMetadata.TestInformation, dataGeneratorMetadata.TestBuilderContext.Current.Events); } - - await ObjectInitializer.InitializeAsync(this); + else + { + await TestObjectInitializer.InitializeAsync(this, TestContext.Current); + } await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata)) { diff --git a/TUnit.Core/Attributes/TestData/AsyncUntypedDataSourceSourceGeneratorAttribute.cs b/TUnit.Core/Attributes/TestData/AsyncUntypedDataSourceSourceGeneratorAttribute.cs index 7dfd686d8d..e505567c21 100644 --- a/TUnit.Core/Attributes/TestData/AsyncUntypedDataSourceSourceGeneratorAttribute.cs +++ b/TUnit.Core/Attributes/TestData/AsyncUntypedDataSourceSourceGeneratorAttribute.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using TUnit.Core.Initialization; namespace TUnit.Core; @@ -11,12 +12,18 @@ public abstract class AsyncUntypedDataSourceGeneratorAttribute : Attribute, IAsy public async IAsyncEnumerable>> GenerateAsync(DataGeneratorMetadata dataGeneratorMetadata) { + // Use centralized TestObjectInitializer for all initialization if (dataGeneratorMetadata is { TestInformation: not null }) { - await PropertyInjectionService.InjectPropertiesIntoObjectAsync(this, dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag, dataGeneratorMetadata.TestInformation, dataGeneratorMetadata.TestBuilderContext.Current.Events); + await TestObjectInitializer.InitializeAsync(this, + dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag, + dataGeneratorMetadata.TestInformation, + dataGeneratorMetadata.TestBuilderContext.Current.Events); + } + else + { + await TestObjectInitializer.InitializeAsync(this, TestContext.Current); } - - await ObjectInitializer.InitializeAsync(this); await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata)) { diff --git a/TUnit.Core/Initialization/TestObjectInitializer.cs b/TUnit.Core/Initialization/TestObjectInitializer.cs new file mode 100644 index 0000000000..54780311b9 --- /dev/null +++ b/TUnit.Core/Initialization/TestObjectInitializer.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using TUnit.Core.Interfaces; +using TUnit.Core.Tracking; + +namespace TUnit.Core.Initialization; + +/// +/// Centralized service for initializing test-related objects. +/// Provides a single entry point for the complete object initialization lifecycle. +/// +public static class TestObjectInitializer +{ + /// + /// Initializes a single object with the complete lifecycle: + /// Create → Inject Properties → Initialize → Track → Ready + /// + public static async Task InitializeAsync( + T instance, + TestContext? testContext = null) where T : notnull + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + var context = PrepareContext(testContext); + await InitializeObjectAsync(instance, context); + return instance; + } + + /// + /// Initializes a single object with explicit context parameters. + /// + public static async Task InitializeAsync( + object instance, + Dictionary? objectBag = null, + MethodMetadata? methodMetadata = null, + TestContextEvents? events = null) + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + var context = new InitializationContext + { + ObjectBag = objectBag ?? new Dictionary(), + MethodMetadata = methodMetadata, + Events = events ?? new TestContextEvents(), + TestContext = TestContext.Current + }; + + await InitializeObjectAsync(instance, context); + } + + /// + /// Initializes multiple objects (e.g., test arguments) in parallel. + /// + public static async Task InitializeArgumentsAsync( + object?[] arguments, + Dictionary objectBag, + MethodMetadata methodMetadata, + TestContextEvents events) + { + if (arguments == null || arguments.Length == 0) + { + return; + } + + var context = new InitializationContext + { + ObjectBag = objectBag, + MethodMetadata = methodMetadata, + Events = events, + TestContext = TestContext.Current + }; + + // Process arguments in parallel for performance + var tasks = new List(); + foreach (var argument in arguments) + { + if (argument != null) + { + tasks.Add(InitializeObjectAsync(argument, context)); + } + } + + await Task.WhenAll(tasks); + } + + /// + /// Initializes test class instance with full lifecycle. + /// + public static async Task InitializeTestClassAsync( + object testClassInstance, + TestContext testContext) + { + if (testClassInstance == null) + { + throw new ArgumentNullException(nameof(testClassInstance)); + } + + var context = PrepareContext(testContext); + + // Track the test class instance + ObjectTracker.TrackObject(context.Events, testClassInstance); + + // Initialize the instance + await InitializeObjectAsync(testClassInstance, context); + } + + /// + /// Core initialization logic - the single place where all initialization happens. + /// + private static async Task InitializeObjectAsync(object instance, InitializationContext context) + { + try + { + // Step 1: Property Injection + if (RequiresPropertyInjection(instance)) + { + await PropertyInjectionService.InjectPropertiesIntoObjectAsync( + instance, + context.ObjectBag, + context.MethodMetadata, + context.Events); + } + + // Step 2: Object Initialization (IAsyncInitializer) + if (instance is IAsyncInitializer asyncInitializer) + { + await ObjectInitializer.InitializeAsync(instance); + } + + // Step 3: Tracking (if not already tracked) + TrackObject(instance, context); + + // Step 4: Post-initialization hooks (future extension point) + await OnObjectInitializedAsync(instance, context); + } + catch (Exception ex) + { + throw new TestObjectInitializationException( + $"Failed to initialize object of type '{instance.GetType().Name}': {ex.Message}", ex); + } + } + + /// + /// Determines if an object requires property injection. + /// + private static bool RequiresPropertyInjection(object instance) + { + // Use the existing cache from PropertyInjectionCache + return PropertyInjection.PropertyInjectionCache.HasInjectableProperties(instance.GetType()); + } + + /// + /// Tracks an object for disposal and ownership. + /// + private static void TrackObject(object instance, InitializationContext context) + { + // Only track if we have events context + if (context.Events != null) + { + ObjectTracker.TrackObject(context.Events, instance); + } + } + + /// + /// Hook for post-initialization processing. + /// + private static Task OnObjectInitializedAsync(object instance, InitializationContext context) + { + // Extension point for future features (e.g., validation, logging) + return Task.CompletedTask; + } + + /// + /// Prepares initialization context from test context. + /// + private static InitializationContext PrepareContext(TestContext? testContext) + { + return new InitializationContext + { + ObjectBag = testContext?.ObjectBag ?? new Dictionary(), + MethodMetadata = testContext?.TestDetails?.MethodMetadata, + Events = testContext?.Events ?? new TestContextEvents(), + TestContext = testContext + }; + } + + /// + /// Internal context for initialization. + /// + private class InitializationContext + { + public required Dictionary ObjectBag { get; init; } + public MethodMetadata? MethodMetadata { get; init; } + public required TestContextEvents Events { get; init; } + public TestContext? TestContext { get; init; } + } +} + +/// +/// Exception thrown when test object initialization fails. +/// +public class TestObjectInitializationException : Exception +{ + public TestObjectInitializationException(string message) : base(message) + { + } + + public TestObjectInitializationException(string message, Exception innerException) + : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/ClassMetadataHelper.cs b/TUnit.Core/PropertyInjection/ClassMetadataHelper.cs new file mode 100644 index 0000000000..332702b404 --- /dev/null +++ b/TUnit.Core/PropertyInjection/ClassMetadataHelper.cs @@ -0,0 +1,57 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; + +namespace TUnit.Core.PropertyInjection; + +/// +/// Helper class for creating and managing ClassMetadata instances. +/// Follows DRY principle by consolidating ClassMetadata creation logic. +/// +internal static class ClassMetadataHelper +{ + /// + /// Gets or creates ClassMetadata for the specified type. + /// + [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Metadata creation")] + public static ClassMetadata GetOrCreateClassMetadata( + [DynamicallyAccessedMembers( + DynamicallyAccessedMemberTypes.PublicConstructors | + DynamicallyAccessedMemberTypes.NonPublicConstructors | + DynamicallyAccessedMemberTypes.PublicMethods | + DynamicallyAccessedMemberTypes.NonPublicMethods | + DynamicallyAccessedMemberTypes.PublicProperties)] + 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}", + TypeReference = new TypeReference { AssemblyQualifiedName = p.ParameterType.AssemblyQualifiedName }, + ReflectionInfo = p + }).ToArray() ?? Array.Empty(); + + return new ClassMetadata + { + Type = type, + TypeReference = TypeReference.CreateConcrete(type.AssemblyQualifiedName ?? type.FullName ?? type.Name), + 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 ? GetOrCreateClassMetadata(type.DeclaringType) : null + }; + }); + } +} \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/Initialization/PropertyDataResolver.cs b/TUnit.Core/PropertyInjection/Initialization/PropertyDataResolver.cs new file mode 100644 index 0000000000..ef0f0a8e40 --- /dev/null +++ b/TUnit.Core/PropertyInjection/Initialization/PropertyDataResolver.cs @@ -0,0 +1,133 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using TUnit.Core.Interfaces.SourceGenerator; + +namespace TUnit.Core.PropertyInjection.Initialization; + +/// +/// Handles all data source resolution logic for property initialization. +/// Follows Single Responsibility Principle by focusing only on data resolution. +/// +internal static class PropertyDataResolver +{ + /// + /// Resolves data from a property's data source. + /// + [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Property types handled dynamically")] + public static async Task ResolvePropertyDataAsync(PropertyInitializationContext context) + { + var dataSource = GetDataSource(context); + if (dataSource == null) + { + return null; + } + + var dataGeneratorMetadata = CreateDataGeneratorMetadata(context, dataSource); + var dataRows = dataSource.GetDataRowsAsync(dataGeneratorMetadata); + + // Get the first value from the data source + await foreach (var factory in dataRows) + { + var args = await factory(); + var value = ResolveValueFromArgs(context.PropertyType, args); + + // Resolve any Func wrappers + value = await ResolveDelegateValue(value); + + if (value != null) + { + return value; + } + } + + return null; + } + + /// + /// Gets the data source from the context. + /// + private static IDataSourceAttribute? GetDataSource(PropertyInitializationContext context) + { + if (context.DataSource != null) + { + return context.DataSource; + } + + if (context.SourceGeneratedMetadata != null) + { + return context.SourceGeneratedMetadata.CreateDataSource(); + } + + return null; + } + + /// + /// Creates data generator metadata for the property. + /// + [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Property injection metadata")] + private static DataGeneratorMetadata CreateDataGeneratorMetadata( + PropertyInitializationContext context, + IDataSourceAttribute dataSource) + { + if (context.SourceGeneratedMetadata != null) + { + // Source-generated mode + var propertyMetadata = new PropertyMetadata + { + IsStatic = false, + Name = context.PropertyName, + ClassMetadata = ClassMetadataHelper.GetOrCreateClassMetadata(context.SourceGeneratedMetadata.ContainingType), + Type = context.PropertyType, + ReflectionInfo = PropertyHelper.GetPropertyInfo(context.SourceGeneratedMetadata.ContainingType, context.PropertyName), + Getter = parent => PropertyHelper.GetPropertyInfo(context.SourceGeneratedMetadata.ContainingType, context.PropertyName).GetValue(parent!)!, + ContainingTypeMetadata = ClassMetadataHelper.GetOrCreateClassMetadata(context.SourceGeneratedMetadata.ContainingType) + }; + + return DataGeneratorMetadataCreator.CreateForPropertyInjection( + propertyMetadata, + context.MethodMetadata, + dataSource, + context.TestContext, + context.TestContext?.TestDetails.ClassInstance, + context.Events, + context.ObjectBag); + } + else if (context.PropertyInfo != null) + { + // Reflection mode + return DataGeneratorMetadataCreator.CreateForPropertyInjection( + context.PropertyInfo, + context.PropertyInfo.DeclaringType!, + context.MethodMetadata, + dataSource, + context.TestContext, + context.Instance, + context.Events, + context.ObjectBag); + } + + throw new InvalidOperationException("Cannot create data generator metadata: no property information available"); + } + + /// + /// Resolves value from data source arguments, handling tuples. + /// + [UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "Tuple types are created dynamically")] + private static object? ResolveValueFromArgs( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + Type propertyType, + object?[]? args) + { + return TupleValueResolver.ResolveTupleValue(propertyType, args); + } + + /// + /// Resolves delegate values by invoking them. + /// + private static async ValueTask ResolveDelegateValue(object? value) + { + return await PropertyValueProcessor.ResolveTestDataValueAsync(typeof(object), value); + } +} \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationContext.cs b/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationContext.cs new file mode 100644 index 0000000000..82e4c83b53 --- /dev/null +++ b/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationContext.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reflection; +using TUnit.Core.Interfaces.SourceGenerator; + +namespace TUnit.Core.PropertyInjection.Initialization; + +/// +/// Encapsulates all context needed for property initialization. +/// Follows Single Responsibility Principle by being a pure data container. +/// +internal sealed class PropertyInitializationContext +{ + /// + /// The object instance whose properties are being initialized. + /// + public required object Instance { get; init; } + + /// + /// Property metadata for source-generated mode. + /// + public PropertyInjectionMetadata? SourceGeneratedMetadata { get; init; } + + /// + /// Property info and data source for reflection mode. + /// + public PropertyInfo? PropertyInfo { get; init; } + + /// + /// Data source attribute for the property. + /// + public IDataSourceAttribute? DataSource { get; init; } + + /// + /// Property name being initialized. + /// + public required string PropertyName { get; init; } + + /// + /// Property type. + /// + public required Type PropertyType { get; init; } + + /// + /// Action to set the property value. + /// + public required Action PropertySetter { get; init; } + + /// + /// Shared object bag for the test context. + /// + public required Dictionary ObjectBag { get; init; } + + /// + /// Method metadata for the test. + /// + public MethodMetadata? MethodMetadata { get; init; } + + /// + /// Test context events for tracking. + /// + public required TestContextEvents Events { get; init; } + + /// + /// Visited objects for cycle detection. + /// + public required ConcurrentDictionary VisitedObjects { get; init; } + + /// + /// Current test context (optional). + /// + public TestContext? TestContext { get; init; } + + /// + /// The resolved value for the property (set during processing). + /// + public object? ResolvedValue { get; set; } + + /// + /// Indicates if this is for nested property initialization. + /// + public bool IsNestedProperty { get; init; } + + /// + /// Parent object for nested properties. + /// + public object? ParentInstance { get; init; } +} \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationOrchestrator.cs b/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationOrchestrator.cs new file mode 100644 index 0000000000..0827977521 --- /dev/null +++ b/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationOrchestrator.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using TUnit.Core.Interfaces.SourceGenerator; + +namespace TUnit.Core.PropertyInjection.Initialization; + +/// +/// Orchestrates the entire property initialization process. +/// Coordinates between different components and manages the initialization flow. +/// +internal sealed class PropertyInitializationOrchestrator +{ + private readonly PropertyInitializationPipeline _pipeline; + + public PropertyInitializationOrchestrator() + { + _pipeline = PropertyInitializationPipeline.CreateDefault(); + } + + /// + /// Initializes all properties for an instance using source-generated metadata. + /// + public async Task InitializePropertiesAsync( + object instance, + PropertyInjectionMetadata[] properties, + Dictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + ConcurrentDictionary visitedObjects) + { + if (properties.Length == 0) + { + return; + } + + var contexts = properties.Select(metadata => CreateContext( + instance, metadata, objectBag, methodMetadata, events, visitedObjects, TestContext.Current)); + + await _pipeline.ExecuteParallelAsync(contexts); + } + + /// + /// Initializes all properties for an instance using reflection. + /// + [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Reflection mode support")] + public async Task InitializePropertiesAsync( + object instance, + (PropertyInfo Property, IDataSourceAttribute DataSource)[] properties, + Dictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + ConcurrentDictionary visitedObjects) + { + if (properties.Length == 0) + { + return; + } + + var contexts = properties.Select(pair => CreateContext( + instance, pair.Property, pair.DataSource, objectBag, methodMetadata, events, visitedObjects, TestContext.Current)); + + await _pipeline.ExecuteParallelAsync(contexts); + } + + /// + /// Handles the complete initialization flow for an object with properties. + /// + public async Task InitializeObjectWithPropertiesAsync( + object instance, + PropertyInjectionPlan plan, + Dictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + ConcurrentDictionary visitedObjects) + { + if (!plan.HasProperties) + { + // No properties to inject, just initialize the object + await ObjectInitializer.InitializeAsync(instance); + return; + } + + // Initialize properties based on the mode + if (SourceRegistrar.IsEnabled) + { + await InitializePropertiesAsync( + instance, plan.SourceGeneratedProperties, objectBag, methodMetadata, events, visitedObjects); + } + else + { + await InitializePropertiesAsync( + instance, plan.ReflectionProperties, objectBag, methodMetadata, events, visitedObjects); + } + + // Initialize the object itself after properties are set + await ObjectInitializer.InitializeAsync(instance); + } + + /// + /// Creates initialization context for source-generated properties. + /// + private PropertyInitializationContext CreateContext( + object instance, + PropertyInjectionMetadata metadata, + Dictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + ConcurrentDictionary visitedObjects, + TestContext? testContext) + { + return new PropertyInitializationContext + { + Instance = instance, + SourceGeneratedMetadata = metadata, + PropertyName = metadata.PropertyName, + PropertyType = metadata.PropertyType, + PropertySetter = metadata.SetProperty, + ObjectBag = objectBag, + MethodMetadata = methodMetadata, + Events = events, + VisitedObjects = visitedObjects, + TestContext = testContext, + IsNestedProperty = false + }; + } + + /// + /// Creates initialization context for reflection-based properties. + /// + private PropertyInitializationContext CreateContext( + object instance, + PropertyInfo property, + IDataSourceAttribute dataSource, + Dictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + ConcurrentDictionary visitedObjects, + TestContext? testContext) + { + return new PropertyInitializationContext + { + Instance = instance, + PropertyInfo = property, + DataSource = dataSource, + PropertyName = property.Name, + PropertyType = property.PropertyType, + PropertySetter = PropertySetterFactory.CreateSetter(property), + ObjectBag = objectBag, + MethodMetadata = methodMetadata, + Events = events, + VisitedObjects = visitedObjects, + TestContext = testContext, + IsNestedProperty = false + }; + } + + /// + /// Gets the singleton instance of the orchestrator. + /// + public static PropertyInitializationOrchestrator Instance { get; } = new(); +} \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationPipeline.cs b/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationPipeline.cs new file mode 100644 index 0000000000..0b64bf2255 --- /dev/null +++ b/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationPipeline.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using TUnit.Core.PropertyInjection.Initialization.Strategies; + +namespace TUnit.Core.PropertyInjection.Initialization; + +/// +/// Defines and executes the property initialization pipeline. +/// Follows Pipeline pattern for clear, sequential processing steps. +/// +internal sealed class PropertyInitializationPipeline +{ + private readonly List _strategies; + private readonly List> _beforeSteps; + private readonly List> _afterSteps; + + public PropertyInitializationPipeline() + { + _strategies = new List + { + new SourceGeneratedPropertyStrategy(), + new ReflectionPropertyStrategy(), + new NestedPropertyStrategy() + }; + + _beforeSteps = new List>(); + _afterSteps = new List>(); + } + + /// + /// Adds a step to execute before property initialization. + /// + public PropertyInitializationPipeline AddBeforeStep(Func step) + { + _beforeSteps.Add(step); + return this; + } + + /// + /// Adds a step to execute after property initialization. + /// + public PropertyInitializationPipeline AddAfterStep(Func step) + { + _afterSteps.Add(step); + return this; + } + + /// + /// Adds a custom strategy to the pipeline. + /// + public PropertyInitializationPipeline AddStrategy(IPropertyInitializationStrategy strategy) + { + _strategies.Add(strategy); + return this; + } + + /// + /// Executes the pipeline for a given context. + /// + public async Task ExecuteAsync(PropertyInitializationContext context) + { + try + { + // Execute before steps + foreach (var step in _beforeSteps) + { + await step(context); + } + + // Find and execute the appropriate strategy + var executed = false; + foreach (var strategy in _strategies) + { + if (strategy.CanHandle(context)) + { + await strategy.InitializePropertyAsync(context); + executed = true; + break; + } + } + + if (!executed && !context.IsNestedProperty) + { + // No strategy could handle this property + throw new InvalidOperationException( + $"No initialization strategy available for property '{context.PropertyName}' " + + $"of type '{context.PropertyType.Name}' on '{context.Instance.GetType().Name}'"); + } + + // Execute after steps + foreach (var step in _afterSteps) + { + await step(context); + } + } + catch (Exception ex) when (!(ex is InvalidOperationException)) + { + throw new InvalidOperationException( + $"Failed to initialize property '{context.PropertyName}': {ex.Message}", ex); + } + } + + /// + /// Executes the pipeline for multiple contexts in parallel. + /// + public async Task ExecuteParallelAsync(IEnumerable contexts) + { + var tasks = contexts.Select(ExecuteAsync); + await Task.WhenAll(tasks); + } + + /// + /// Creates a default pipeline with standard steps. + /// + public static PropertyInitializationPipeline CreateDefault() + { + return new PropertyInitializationPipeline() + .AddBeforeStep(ValidateContext) + .AddAfterStep(FinalizeInitialization); + } + + /// + /// Validates the initialization context before processing. + /// + private static Task ValidateContext(PropertyInitializationContext context) + { + if (context.Instance == null) + { + throw new ArgumentNullException(nameof(context.Instance), "Instance cannot be null"); + } + + if (string.IsNullOrEmpty(context.PropertyName)) + { + throw new ArgumentException("Property name cannot be empty", nameof(context.PropertyName)); + } + + if (context.PropertyType == null) + { + throw new ArgumentNullException(nameof(context.PropertyType), "Property type cannot be null"); + } + + return Task.CompletedTask; + } + + /// + /// Finalizes the initialization after property is set. + /// + private static Task FinalizeInitialization(PropertyInitializationContext context) + { + // Any final cleanup or verification can go here + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/Initialization/PropertyTrackingService.cs b/TUnit.Core/PropertyInjection/Initialization/PropertyTrackingService.cs new file mode 100644 index 0000000000..cf86551ad7 --- /dev/null +++ b/TUnit.Core/PropertyInjection/Initialization/PropertyTrackingService.cs @@ -0,0 +1,144 @@ +using System.Collections.Concurrent; +using System.Threading.Tasks; +using TUnit.Core.Tracking; + +namespace TUnit.Core.PropertyInjection.Initialization; + +/// +/// Centralizes all property tracking operations during initialization. +/// Follows Single Responsibility Principle by handling only tracking concerns. +/// +internal static class PropertyTrackingService +{ + /// + /// Tracks a property value for disposal and ownership. + /// + public static void TrackPropertyValue(PropertyInitializationContext context, object? propertyValue) + { + if (propertyValue == null) + { + return; + } + + // Track the object for disposal + ObjectTracker.TrackObject(context.Events, propertyValue); + + // Track ownership relationship + if (context.ParentInstance != null) + { + ObjectTracker.TrackOwnership(context.ParentInstance, propertyValue); + } + else + { + ObjectTracker.TrackOwnership(context.Instance, propertyValue); + } + } + + /// + /// Handles tracking for nested properties after initialization. + /// + public static async Task TrackNestedPropertiesAsync( + PropertyInitializationContext context, + object propertyValue, + PropertyInjectionPlan plan) + { + if (!plan.HasProperties) + { + return; + } + + if (SourceRegistrar.IsEnabled) + { + await TrackSourceGeneratedNestedProperties(context, propertyValue, plan); + } + else + { + await TrackReflectionNestedProperties(context, propertyValue, plan); + } + } + + /// + /// Tracks nested properties for source-generated mode. + /// + private static async Task TrackSourceGeneratedNestedProperties( + PropertyInitializationContext context, + object instance, + PropertyInjectionPlan plan) + { + foreach (var metadata in plan.SourceGeneratedProperties) + { + var property = metadata.ContainingType.GetProperty(metadata.PropertyName); + if (property != null && property.CanRead) + { + var nestedValue = property.GetValue(instance); + if (nestedValue != null) + { + TrackNestedPropertyValue(context, instance, nestedValue); + await InitializeNestedIfRequired(context, nestedValue); + } + } + } + } + + /// + /// Tracks nested properties for reflection mode. + /// + private static async Task TrackReflectionNestedProperties( + PropertyInitializationContext context, + object instance, + PropertyInjectionPlan plan) + { + foreach (var (property, _) in plan.ReflectionProperties) + { + var nestedValue = property.GetValue(instance); + if (nestedValue != null) + { + TrackNestedPropertyValue(context, instance, nestedValue); + await InitializeNestedIfRequired(context, nestedValue); + } + } + } + + /// + /// Tracks a nested property value. + /// + private static void TrackNestedPropertyValue( + PropertyInitializationContext context, + object parentInstance, + object nestedValue) + { + ObjectTracker.TrackObject(context.Events, nestedValue); + ObjectTracker.TrackOwnership(parentInstance, nestedValue); + } + + /// + /// Initializes nested property if it has injectable properties. + /// + private static async Task InitializeNestedIfRequired( + PropertyInitializationContext context, + object nestedValue) + { + if (PropertyInjectionCache.HasInjectableProperties(nestedValue.GetType())) + { + // This will be handled by the nested property strategy + // Just mark it for processing + context.VisitedObjects.TryAdd(nestedValue, 1); + } + else + { + // No nested properties, just initialize + await ObjectInitializer.InitializeAsync(nestedValue); + } + } + + /// + /// Adds property value to test context tracking. + /// + public static void AddToTestContext(PropertyInitializationContext context, object? propertyValue) + { + if (context.TestContext != null && propertyValue != null) + { + context.TestContext.TestDetails.TestClassInjectedPropertyArguments[context.PropertyName] = propertyValue; + } + } +} \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/Initialization/Strategies/IPropertyInitializationStrategy.cs b/TUnit.Core/PropertyInjection/Initialization/Strategies/IPropertyInitializationStrategy.cs new file mode 100644 index 0000000000..6944def5bc --- /dev/null +++ b/TUnit.Core/PropertyInjection/Initialization/Strategies/IPropertyInitializationStrategy.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace TUnit.Core.PropertyInjection.Initialization.Strategies; + +/// +/// Defines the contract for property initialization strategies. +/// Follows Strategy pattern for flexible initialization approaches. +/// +internal interface IPropertyInitializationStrategy +{ + /// + /// Determines if this strategy can handle the given context. + /// + bool CanHandle(PropertyInitializationContext context); + + /// + /// Initializes a property based on the provided context. + /// + Task InitializePropertyAsync(PropertyInitializationContext context); +} \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/Initialization/Strategies/NestedPropertyStrategy.cs b/TUnit.Core/PropertyInjection/Initialization/Strategies/NestedPropertyStrategy.cs new file mode 100644 index 0000000000..afb4abd271 --- /dev/null +++ b/TUnit.Core/PropertyInjection/Initialization/Strategies/NestedPropertyStrategy.cs @@ -0,0 +1,160 @@ +using System.Threading.Tasks; + +namespace TUnit.Core.PropertyInjection.Initialization.Strategies; + +/// +/// Strategy for handling nested property initialization. +/// Manages recursive property injection for complex object graphs. +/// +internal sealed class NestedPropertyStrategy : IPropertyInitializationStrategy +{ + /// + /// Determines if this strategy can handle nested properties. + /// + public bool CanHandle(PropertyInitializationContext context) + { + return context.IsNestedProperty && context.ResolvedValue != null; + } + + /// + /// Initializes nested properties within an already resolved property value. + /// + public async Task InitializePropertyAsync(PropertyInitializationContext context) + { + if (context.ResolvedValue == null) + { + return; + } + + var propertyValue = context.ResolvedValue; + var propertyType = propertyValue.GetType(); + + // Check if we've already processed this object (cycle detection) + if (!context.VisitedObjects.TryAdd(propertyValue, 0)) + { + return; // Already processing or processed + } + + // Get the injection plan for this type + var plan = PropertyInjectionCache.GetOrCreatePlan(propertyType); + + if (!plan.HasProperties) + { + // No nested properties to inject, just initialize the object + await ObjectInitializer.InitializeAsync(propertyValue); + return; + } + + // Track the property value and its nested properties + await PropertyTrackingService.TrackNestedPropertiesAsync(context, propertyValue, plan); + + // Recursively inject properties into the nested object + if (SourceRegistrar.IsEnabled) + { + await ProcessSourceGeneratedNestedProperties(context, propertyValue, plan); + } + else + { + await ProcessReflectionNestedProperties(context, propertyValue, plan); + } + + // Initialize the object after all properties are set + await ObjectInitializer.InitializeAsync(propertyValue); + } + + /// + /// Processes nested properties using source-generated metadata. + /// + private async Task ProcessSourceGeneratedNestedProperties( + PropertyInitializationContext parentContext, + object instance, + PropertyInjectionPlan plan) + { + var tasks = plan.SourceGeneratedProperties.Select(async metadata => + { + var nestedContext = CreateNestedContext(parentContext, instance, metadata); + var strategy = new SourceGeneratedPropertyStrategy(); + + if (strategy.CanHandle(nestedContext)) + { + await strategy.InitializePropertyAsync(nestedContext); + } + }); + + await Task.WhenAll(tasks); + } + + /// + /// Processes nested properties using reflection. + /// + private async Task ProcessReflectionNestedProperties( + PropertyInitializationContext parentContext, + object instance, + PropertyInjectionPlan plan) + { + var tasks = plan.ReflectionProperties.Select(async pair => + { + var nestedContext = CreateNestedContext(parentContext, instance, pair.Property, pair.DataSource); + var strategy = new ReflectionPropertyStrategy(); + + if (strategy.CanHandle(nestedContext)) + { + await strategy.InitializePropertyAsync(nestedContext); + } + }); + + await Task.WhenAll(tasks); + } + + /// + /// Creates a nested context for source-generated properties. + /// + private PropertyInitializationContext CreateNestedContext( + PropertyInitializationContext parentContext, + object instance, + Interfaces.SourceGenerator.PropertyInjectionMetadata metadata) + { + return new PropertyInitializationContext + { + Instance = instance, + SourceGeneratedMetadata = metadata, + PropertyName = metadata.PropertyName, + PropertyType = metadata.PropertyType, + PropertySetter = metadata.SetProperty, + ObjectBag = parentContext.ObjectBag, + MethodMetadata = parentContext.MethodMetadata, + Events = parentContext.Events, + VisitedObjects = parentContext.VisitedObjects, + TestContext = parentContext.TestContext, + IsNestedProperty = true, + ParentInstance = parentContext.Instance + }; + } + + /// + /// Creates a nested context for reflection-based properties. + /// + private PropertyInitializationContext CreateNestedContext( + PropertyInitializationContext parentContext, + object instance, + System.Reflection.PropertyInfo property, + IDataSourceAttribute dataSource) + { + return new PropertyInitializationContext + { + Instance = instance, + PropertyInfo = property, + DataSource = dataSource, + PropertyName = property.Name, + PropertyType = property.PropertyType, + PropertySetter = PropertySetterFactory.CreateSetter(property), + ObjectBag = parentContext.ObjectBag, + MethodMetadata = parentContext.MethodMetadata, + Events = parentContext.Events, + VisitedObjects = parentContext.VisitedObjects, + TestContext = parentContext.TestContext, + IsNestedProperty = true, + ParentInstance = parentContext.Instance + }; + } +} \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/Initialization/Strategies/ReflectionPropertyStrategy.cs b/TUnit.Core/PropertyInjection/Initialization/Strategies/ReflectionPropertyStrategy.cs new file mode 100644 index 0000000000..58501ed984 --- /dev/null +++ b/TUnit.Core/PropertyInjection/Initialization/Strategies/ReflectionPropertyStrategy.cs @@ -0,0 +1,75 @@ +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace TUnit.Core.PropertyInjection.Initialization.Strategies; + +/// +/// Strategy for initializing properties using reflection. +/// Used when source generation is not available. +/// +internal sealed class ReflectionPropertyStrategy : IPropertyInitializationStrategy +{ + /// + /// Determines if this strategy can handle reflection-based properties. + /// + public bool CanHandle(PropertyInitializationContext context) + { + return context.PropertyInfo != null && context.DataSource != null && !SourceRegistrar.IsEnabled; + } + + /// + /// Initializes a property using reflection. + /// + [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Reflection mode support")] + public async Task InitializePropertyAsync(PropertyInitializationContext context) + { + if (context.PropertyInfo == null || context.DataSource == null) + { + return; + } + + // Step 1: Resolve data from the data source + var resolvedValue = await PropertyDataResolver.ResolvePropertyDataAsync(context); + if (resolvedValue == null) + { + return; + } + + context.ResolvedValue = resolvedValue; + + // Step 2: Track the property value + PropertyTrackingService.TrackPropertyValue(context, resolvedValue); + + // Step 3: Handle nested property initialization + if (PropertyInjectionCache.HasInjectableProperties(resolvedValue.GetType())) + { + // Mark for recursive processing + await InitializeNestedProperties(context, resolvedValue); + } + else + { + // Just initialize the object + await ObjectInitializer.InitializeAsync(resolvedValue); + } + + // Step 4: Set the property value + context.PropertySetter(context.Instance, resolvedValue); + + // Step 5: Add to test context tracking + PropertyTrackingService.AddToTestContext(context, resolvedValue); + } + + /// + /// Handles initialization of nested properties. + /// + private async Task InitializeNestedProperties(PropertyInitializationContext context, object propertyValue) + { + // This will be handled by the PropertyInjectionService recursively + // We just need to ensure it's initialized + await PropertyInjectionService.InjectPropertiesIntoObjectAsync( + propertyValue, + context.ObjectBag, + context.MethodMetadata, + context.Events); + } +} \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/Initialization/Strategies/SourceGeneratedPropertyStrategy.cs b/TUnit.Core/PropertyInjection/Initialization/Strategies/SourceGeneratedPropertyStrategy.cs new file mode 100644 index 0000000000..12a17eb585 --- /dev/null +++ b/TUnit.Core/PropertyInjection/Initialization/Strategies/SourceGeneratedPropertyStrategy.cs @@ -0,0 +1,73 @@ +using System.Threading.Tasks; + +namespace TUnit.Core.PropertyInjection.Initialization.Strategies; + +/// +/// Strategy for initializing properties using source-generated metadata. +/// Optimized for AOT and performance. +/// +internal sealed class SourceGeneratedPropertyStrategy : IPropertyInitializationStrategy +{ + /// + /// Determines if this strategy can handle source-generated properties. + /// + public bool CanHandle(PropertyInitializationContext context) + { + return context.SourceGeneratedMetadata != null && SourceRegistrar.IsEnabled; + } + + /// + /// Initializes a property using source-generated metadata. + /// + public async Task InitializePropertyAsync(PropertyInitializationContext context) + { + if (context.SourceGeneratedMetadata == null) + { + return; + } + + // Step 1: Resolve data from the data source + var resolvedValue = await PropertyDataResolver.ResolvePropertyDataAsync(context); + if (resolvedValue == null) + { + return; + } + + context.ResolvedValue = resolvedValue; + + // Step 2: Track the property value + PropertyTrackingService.TrackPropertyValue(context, resolvedValue); + + // Step 3: Handle nested property initialization + if (PropertyInjectionCache.HasInjectableProperties(resolvedValue.GetType())) + { + // Mark for recursive processing + await InitializeNestedProperties(context, resolvedValue); + } + else + { + // Just initialize the object + await ObjectInitializer.InitializeAsync(resolvedValue); + } + + // Step 4: Set the property value + context.SourceGeneratedMetadata.SetProperty(context.Instance, resolvedValue); + + // Step 5: Add to test context tracking + PropertyTrackingService.AddToTestContext(context, resolvedValue); + } + + /// + /// Handles initialization of nested properties. + /// + private async Task InitializeNestedProperties(PropertyInitializationContext context, object propertyValue) + { + // This will be handled by the PropertyInjectionService recursively + // We just need to ensure it's initialized + await PropertyInjectionService.InjectPropertiesIntoObjectAsync( + propertyValue, + context.ObjectBag, + context.MethodMetadata, + context.Events); + } +} \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/PropertyHelper.cs b/TUnit.Core/PropertyInjection/PropertyHelper.cs new file mode 100644 index 0000000000..e4dd460196 --- /dev/null +++ b/TUnit.Core/PropertyInjection/PropertyHelper.cs @@ -0,0 +1,31 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace TUnit.Core.PropertyInjection; + +/// +/// Helper class for property-related operations. +/// Consolidates property reflection logic in one place. +/// +internal static class PropertyHelper +{ + /// + /// Gets PropertyInfo in an AOT-safe manner. + /// + public static PropertyInfo GetPropertyInfo( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + Type containingType, + string propertyName) + { + var property = containingType.GetProperty(propertyName); + + if (property == null) + { + throw new InvalidOperationException( + $"Property '{propertyName}' not found on type '{containingType.Name}'"); + } + + return property; + } +} diff --git a/TUnit.Core/PropertyInjection/PropertyInjectionCache.cs b/TUnit.Core/PropertyInjection/PropertyInjectionCache.cs new file mode 100644 index 0000000000..72e09edee5 --- /dev/null +++ b/TUnit.Core/PropertyInjection/PropertyInjectionCache.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading.Tasks; +using TUnit.Core.Data; + +namespace TUnit.Core.PropertyInjection; + +/// +/// Provides caching functionality for property injection operations. +/// Follows Single Responsibility Principle by focusing only on caching. +/// +internal static class PropertyInjectionCache +{ + private static readonly ThreadSafeDictionary _injectionPlans = new(); + private static readonly ThreadSafeDictionary _shouldInjectCache = new(); + private static readonly ThreadSafeDictionary _injectionTasks = new(); + + /// + /// Gets or creates an injection plan for the specified type. + /// + public static PropertyInjectionPlan GetOrCreatePlan(Type type) + { + return _injectionPlans.GetOrAdd(type, _ => PropertyInjectionPlanBuilder.Build(type)); + } + + /// + /// Checks if a type has injectable properties using caching. + /// + public static bool HasInjectableProperties(Type type) + { + return _shouldInjectCache.GetOrAdd(type, t => + { + var plan = GetOrCreatePlan(t); + return plan.HasProperties; + }); + } + + /// + /// Gets or adds an injection task for the specified instance. + /// + public static async Task GetOrAddInjectionTask(object instance, Func taskFactory) + { + await _injectionTasks.GetOrAdd(instance, taskFactory); + } + + /// + /// Checks if an injection task already exists for the instance. + /// + public static bool TryGetInjectionTask(object instance, out Task? existingTask) + { + return _injectionTasks.TryGetValue(instance, out existingTask); + } +} \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/PropertyInjectionPlanBuilder.cs b/TUnit.Core/PropertyInjection/PropertyInjectionPlanBuilder.cs new file mode 100644 index 0000000000..b27c854a23 --- /dev/null +++ b/TUnit.Core/PropertyInjection/PropertyInjectionPlanBuilder.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using TUnit.Core.Interfaces.SourceGenerator; + +namespace TUnit.Core.PropertyInjection; + +/// +/// Responsible for building property injection plans for types. +/// Follows Single Responsibility Principle by focusing only on plan creation. +/// +internal static class PropertyInjectionPlanBuilder +{ + /// + /// Creates an injection plan for source-generated mode. + /// + public static PropertyInjectionPlan BuildSourceGeneratedPlan(Type type) + { + var propertySource = PropertySourceRegistry.GetSource(type); + var sourceGenProps = propertySource?.ShouldInitialize == true + ? propertySource.GetPropertyMetadata().ToArray() + : Array.Empty(); + + return new PropertyInjectionPlan + { + Type = type, + SourceGeneratedProperties = sourceGenProps, + ReflectionProperties = Array.Empty<(PropertyInfo, IDataSourceAttribute)>(), + HasProperties = sourceGenProps.Length > 0 + }; + } + + /// + /// Creates an injection plan for reflection mode. + /// + [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Reflection mode support")] + public static PropertyInjectionPlan BuildReflectionPlan(Type type) + { + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) + .Where(p => p.CanWrite || p.SetMethod?.IsPublic == false); // Include init-only properties + + var propertyDataSourcePairs = new List<(PropertyInfo property, IDataSourceAttribute dataSource)>(); + + foreach (var property in properties) + { + foreach (var attr in property.GetCustomAttributes()) + { + if (attr is IDataSourceAttribute dataSourceAttr) + { + propertyDataSourcePairs.Add((property, dataSourceAttr)); + } + } + } + + return new PropertyInjectionPlan + { + Type = type, + SourceGeneratedProperties = Array.Empty(), + ReflectionProperties = propertyDataSourcePairs.ToArray(), + HasProperties = propertyDataSourcePairs.Count > 0 + }; + } + + /// + /// Builds an injection plan based on the current execution mode. + /// + [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Handles both AOT and non-AOT scenarios")] + public static PropertyInjectionPlan Build(Type type) + { + return SourceRegistrar.IsEnabled + ? BuildSourceGeneratedPlan(type) + : BuildReflectionPlan(type); + } +} + +/// +/// Represents a plan for injecting properties into an object. +/// +internal sealed class PropertyInjectionPlan +{ + public required Type Type { get; init; } + public required PropertyInjectionMetadata[] SourceGeneratedProperties { get; init; } + public required (PropertyInfo Property, IDataSourceAttribute DataSource)[] ReflectionProperties { get; init; } + public required bool HasProperties { get; init; } +} \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/PropertySetterFactory.cs b/TUnit.Core/PropertyInjection/PropertySetterFactory.cs new file mode 100644 index 0000000000..ef82b94b4d --- /dev/null +++ b/TUnit.Core/PropertyInjection/PropertySetterFactory.cs @@ -0,0 +1,108 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace TUnit.Core.PropertyInjection; + +/// +/// Factory for creating property setters. +/// Consolidates all property setter creation logic in one place following DRY principle. +/// +internal static class PropertySetterFactory +{ + /// + /// Creates a setter delegate for the given property. + /// + public static Action CreateSetter(PropertyInfo property) + { + if (property.CanWrite && property.SetMethod != null) + { +#if NETSTANDARD2_0 + return (instance, value) => property.SetValue(instance, value); +#else + var setMethod = property.SetMethod; + var isInitOnly = IsInitOnlyMethod(setMethod); + + if (!isInitOnly) + { + return (instance, value) => property.SetValue(instance, value); + } +#endif + } + + var backingField = GetBackingField(property); + if (backingField != null) + { + return (instance, value) => backingField.SetValue(instance, value); + } + + throw new InvalidOperationException( + $"Property '{property.Name}' on type '{property.DeclaringType?.Name}' " + + $"is not writable and no backing field was found."); + } + + /// + /// Gets the backing field for a property. + /// + [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Reflection fallback")] + [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Reflection fallback")] + private static FieldInfo? GetBackingField(PropertyInfo property) + { + var declaringType = property.DeclaringType; + if (declaringType == null) + { + return null; + } + + var backingFieldFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy; + + // Try compiler-generated backing field name + var backingFieldName = $"<{property.Name}>k__BackingField"; + var field = GetFieldSafe(declaringType, backingFieldName, backingFieldFlags); + if (field != null) + { + return field; + } + + // Try underscore-prefixed camelCase name + var underscoreName = "_" + char.ToLowerInvariant(property.Name[0]) + property.Name.Substring(1); + field = GetFieldSafe(declaringType, underscoreName, backingFieldFlags); + if (field != null && field.FieldType == property.PropertyType) + { + return field; + } + + // Try exact property name + field = GetFieldSafe(declaringType, property.Name, backingFieldFlags); + if (field != null && field.FieldType == property.PropertyType) + { + return field; + } + + return null; + } + + /// + /// Helper method to get field with proper trimming suppression. + /// + [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Reflection fallback")] + private static FieldInfo? GetFieldSafe( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)] + Type type, + string name, + BindingFlags bindingFlags) + { + return type.GetField(name, bindingFlags); + } + + /// + /// Checks if a method is init-only. + /// + [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Reflection check")] + private static bool IsInitOnlyMethod(MethodInfo setMethod) + { + var methodType = setMethod.GetType(); + var isInitOnlyProperty = methodType.GetProperty("IsInitOnly"); + return isInitOnlyProperty != null && (bool)isInitOnlyProperty.GetValue(setMethod)!; + } +} \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/PropertyValueProcessor.cs b/TUnit.Core/PropertyInjection/PropertyValueProcessor.cs new file mode 100644 index 0000000000..2dbeab5a41 --- /dev/null +++ b/TUnit.Core/PropertyInjection/PropertyValueProcessor.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using TUnit.Core.Services; +using TUnit.Core.Tracking; + +namespace TUnit.Core.PropertyInjection; + +/// +/// Processes property values during injection. +/// Handles value resolution, tracking, and recursive injection. +/// +internal sealed class PropertyValueProcessor +{ + public PropertyValueProcessor() + { + } + + /// + /// Processes a single injected property value: tracks it, initializes it, sets it on the instance. + /// + public async Task ProcessInjectedValueAsync( + object instance, + object? propertyValue, + Action setProperty, + Dictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + ConcurrentDictionary visitedObjects) + { + if (propertyValue == null) + { + return; + } + + // Track the object for disposal + ObjectTracker.TrackObject(events, propertyValue); + ObjectTracker.TrackOwnership(instance, propertyValue); + + // Check if the property value itself needs property injection + if (ShouldInjectProperties(propertyValue)) + { + // Recursively inject properties into the property value + await PropertyInjectionService.InjectPropertiesIntoObjectAsync( + propertyValue, objectBag, methodMetadata, events); + } + else + { + // Just initialize the object + await ObjectInitializer.InitializeAsync(propertyValue); + } + + // Set the property value on the instance + setProperty(instance, propertyValue); + } + + /// + /// Resolves Func values by invoking them without using reflection (AOT-safe). + /// + public static ValueTask ResolveTestDataValueAsync(Type type, object? value) + { + if (value == null) + { + return new ValueTask(result: null); + } + + if (value is Delegate del) + { + // Use DynamicInvoke which is AOT-safe for parameterless delegates + var result = del.DynamicInvoke(); + return new ValueTask(result); + } + + return new ValueTask(value); + } + + /// + /// Determines if an object should have properties injected based on whether it has properties with data source attributes. + /// + private static bool ShouldInjectProperties(object? obj) + { + if (obj == null) + { + return false; + } + + var type = obj.GetType(); + + // Check if this type has any injectable properties + // This will use cached results from PropertyInjectionService + return PropertyInjectionCache.HasInjectableProperties(type); + } +} \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/TupleValueResolver.cs b/TUnit.Core/PropertyInjection/TupleValueResolver.cs new file mode 100644 index 0000000000..8cb2ac83c3 --- /dev/null +++ b/TUnit.Core/PropertyInjection/TupleValueResolver.cs @@ -0,0 +1,64 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using TUnit.Core.Helpers; + +namespace TUnit.Core.PropertyInjection; + +/// +/// Responsible for resolving tuple values for property injection. +/// Handles tuple creation and type conversion following DRY principle. +/// +internal static class TupleValueResolver +{ + /// + /// Resolves a tuple value from data source arguments for a specific property type. + /// + /// The expected property type + /// The arguments from the data source + /// The resolved value, potentially a tuple + [UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "Tuple types are created dynamically")] + public static object? ResolveTupleValue( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + Type propertyType, + object?[]? args) + { + if (args == null) + { + return null; + } + + // Handle non-tuple properties + if (!TupleFactory.IsTupleType(propertyType)) + { + return args.FirstOrDefault(); + } + + // Handle tuple properties + if (args.Length > 1) + { + // Multiple arguments - create tuple from them + return TupleFactory.CreateTuple(propertyType, args); + } + + if (args.Length == 1 && args[0] != null && TupleFactory.IsTupleType(args[0]!.GetType())) + { + // Single tuple argument - check if it needs type conversion + var tupleValue = args[0]!; + var tupleType = tupleValue.GetType(); + + if (tupleType != propertyType) + { + // Tuple types don't match - unwrap and recreate with correct types + var elements = DataSourceHelpers.UnwrapTupleAot(tupleValue); + return TupleFactory.CreateTuple(propertyType, elements); + } + + // Types match - use directly + return tupleValue; + } + + // Single non-tuple argument for tuple property - shouldn't happen but handle gracefully + return args.FirstOrDefault(); + } +} \ No newline at end of file diff --git a/TUnit.Core/PropertyInjectionService.cs b/TUnit.Core/PropertyInjectionService.cs index 5fadb44f01..33199ef79b 100644 --- a/TUnit.Core/PropertyInjectionService.cs +++ b/TUnit.Core/PropertyInjectionService.cs @@ -6,31 +6,43 @@ using TUnit.Core.Enums; using TUnit.Core.Services; using TUnit.Core.Helpers; +using TUnit.Core.PropertyInjection; +using TUnit.Core.PropertyInjection.Initialization; using System.Reflection; using System.Collections.Concurrent; namespace TUnit.Core; -internal sealed class PropertyInjectionPlan +/// +/// Internal service for property injection. +/// Should only be called through TestObjectInitializer for centralized initialization. +/// +internal sealed class PropertyInjectionService { - public required Type Type { get; init; } - public required PropertyInjectionMetadata[] SourceGeneratedProperties { get; init; } - public required (PropertyInfo Property, IDataSourceAttribute DataSource)[] ReflectionProperties { get; init; } - public required bool HasProperties { get; init; } -} + private static readonly PropertyInjectionService _instance = new(); + private readonly PropertyInitializationOrchestrator _orchestrator; -public sealed class PropertyInjectionService -{ - private static readonly ThreadSafeDictionary _injectionTasks = new(); - private static readonly ThreadSafeDictionary _injectionPlans = new(); - private static readonly ThreadSafeDictionary _shouldInjectCache = new(); + public PropertyInjectionService() + { + _orchestrator = PropertyInitializationOrchestrator.Instance; + } + + /// + /// Gets the singleton instance of the PropertyInjectionService. + /// + public static PropertyInjectionService Instance => _instance; /// /// Injects properties with data sources into argument objects just before test execution. /// This ensures properties are only initialized when the test is about to run. /// Arguments are processed in parallel for better performance. /// - public static async Task InjectPropertiesIntoArgumentsAsync(object?[] arguments, Dictionary objectBag, MethodMetadata methodMetadata, TestContextEvents events) + public static Task InjectPropertiesIntoArgumentsAsync(object?[] arguments, Dictionary objectBag, MethodMetadata methodMetadata, TestContextEvents events) + { + return _instance.InjectPropertiesIntoArgumentsAsyncCore(arguments, objectBag, methodMetadata, events); + } + + private async Task InjectPropertiesIntoArgumentsAsyncCore(object?[] arguments, Dictionary objectBag, MethodMetadata methodMetadata, TestContextEvents events) { if (arguments.Length == 0) { @@ -39,7 +51,7 @@ public static async Task InjectPropertiesIntoArgumentsAsync(object?[] arguments, // Fast path: check if any arguments need injection var injectableArgs = arguments - .Where(argument => argument != null && ShouldInjectProperties(argument)) + .Where(argument => argument != null && PropertyInjectionCache.HasInjectableProperties(argument.GetType())) .ToArray(); if (injectableArgs.Length == 0) @@ -55,25 +67,6 @@ public static async Task InjectPropertiesIntoArgumentsAsync(object?[] arguments, await Task.WhenAll(argumentTasks); } - /// - /// Determines if an object should have properties injected based on whether it has properties with data source attributes. - /// - private static bool ShouldInjectProperties(object? obj) - { - if (obj == null) - { - return false; - } - - var type = obj.GetType(); - - // Use cached result for better performance - check if the type has injectable properties - return _shouldInjectCache.GetOrAdd(type, t => - { - var plan = GetOrCreateInjectionPlan(t); - return plan.HasProperties; - }); - } /// /// Recursively injects properties with data sources into a single object. @@ -81,6 +74,11 @@ private static bool ShouldInjectProperties(object? obj) /// After injection, handles tracking, initialization, and recursive injection. /// public static Task InjectPropertiesIntoObjectAsync(object instance, Dictionary? objectBag, MethodMetadata? methodMetadata, TestContextEvents? events) + { + return _instance.InjectPropertiesIntoObjectAsyncInstance(instance, objectBag, methodMetadata, events); + } + + private Task InjectPropertiesIntoObjectAsyncInstance(object instance, Dictionary? objectBag, MethodMetadata? methodMetadata, TestContextEvents? events) { // Start with an empty visited set for cycle detection #if NETSTANDARD2_0 @@ -91,7 +89,7 @@ public static Task InjectPropertiesIntoObjectAsync(object instance, Dictionary? objectBag, MethodMetadata? methodMetadata, TestContextEvents? events, ConcurrentDictionary visitedObjects) + internal async Task InjectPropertiesIntoObjectAsyncCore(object instance, Dictionary? objectBag, MethodMetadata? methodMetadata, TestContextEvents? events, ConcurrentDictionary visitedObjects) { if (instance == null) { @@ -115,13 +113,13 @@ private static async Task InjectPropertiesIntoObjectAsyncCore(object instance, D try { - var alreadyProcessed = _injectionTasks.TryGetValue(instance, out var existingTask); + var alreadyProcessed = PropertyInjectionCache.TryGetInjectionTask(instance, out var existingTask); if (alreadyProcessed && existingTask != null) { await existingTask; - var plan = GetOrCreateInjectionPlan(instance.GetType()); + var plan = PropertyInjectionCache.GetOrCreatePlan(instance.GetType()); if (plan.HasProperties) { if (SourceRegistrar.IsEnabled) @@ -137,7 +135,7 @@ private static async Task InjectPropertiesIntoObjectAsyncCore(object instance, D ObjectTracker.TrackObject(events, propertyValue); ObjectTracker.TrackOwnership(instance, propertyValue); - if (ShouldInjectProperties(propertyValue)) + if (PropertyInjectionCache.HasInjectableProperties(propertyValue.GetType())) { await InjectPropertiesIntoObjectAsyncCore(propertyValue, objectBag, methodMetadata, events, visitedObjects); } @@ -155,7 +153,7 @@ private static async Task InjectPropertiesIntoObjectAsyncCore(object instance, D ObjectTracker.TrackObject(events, propertyValue); ObjectTracker.TrackOwnership(instance, propertyValue); - if (ShouldInjectProperties(propertyValue)) + if (PropertyInjectionCache.HasInjectableProperties(propertyValue.GetType())) { await InjectPropertiesIntoObjectAsyncCore(propertyValue, objectBag, methodMetadata, events, visitedObjects); } @@ -166,26 +164,13 @@ private static async Task InjectPropertiesIntoObjectAsyncCore(object instance, D } else { - await _injectionTasks.GetOrAdd(instance, async _ => + await PropertyInjectionCache.GetOrAddInjectionTask(instance, async _ => { - var plan = GetOrCreateInjectionPlan(instance.GetType()); - - if (!plan.HasProperties) - { - await ObjectInitializer.InitializeAsync(instance); - return; - } - - if (SourceRegistrar.IsEnabled) - { - await InjectPropertiesUsingPlanAsync(instance, plan.SourceGeneratedProperties, objectBag, methodMetadata, events, visitedObjects); - } - else - { - await InjectPropertiesUsingReflectionPlanAsync(instance, plan.ReflectionProperties, objectBag, methodMetadata, events, visitedObjects); - } + var plan = PropertyInjectionCache.GetOrCreatePlan(instance.GetType()); - await ObjectInitializer.InitializeAsync(instance); + // Use the new orchestrator for property initialization + await _orchestrator.InitializeObjectWithPropertiesAsync( + instance, plan, objectBag, methodMetadata, events, visitedObjects); }); } } @@ -195,609 +180,7 @@ await _injectionTasks.GetOrAdd(instance, async _ => } } - /// - /// Creates or retrieves a cached injection plan for a type. - /// - [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "This method is part of the optimization and handles both AOT and non-AOT scenarios")] - private static PropertyInjectionPlan GetOrCreateInjectionPlan(Type type) - { - return _injectionPlans.GetOrAdd(type, _ => - { - if (SourceRegistrar.IsEnabled) - { - var propertySource = PropertySourceRegistry.GetSource(type); - var sourceGenProps = propertySource?.ShouldInitialize == true - ? propertySource.GetPropertyMetadata().ToArray() - : Array.Empty(); - - return new PropertyInjectionPlan - { - Type = type, - SourceGeneratedProperties = sourceGenProps, - ReflectionProperties = Array.Empty<(PropertyInfo, IDataSourceAttribute)>(), - HasProperties = sourceGenProps.Length > 0 - }; - } - else - { - var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) - .Where(p => p.CanWrite || p.SetMethod?.IsPublic == false); // Include init-only properties - - var propertyDataSourcePairs = new List<(PropertyInfo property, IDataSourceAttribute dataSource)>(); - - foreach (var property in properties) - { - foreach (var attr in property.GetCustomAttributes()) - { - if (attr is IDataSourceAttribute dataSourceAttr) - { - propertyDataSourcePairs.Add((property, dataSourceAttr)); - } - } - } - - return new PropertyInjectionPlan - { - Type = type, - SourceGeneratedProperties = Array.Empty(), - ReflectionProperties = propertyDataSourcePairs.ToArray(), - HasProperties = propertyDataSourcePairs.Count > 0 - }; - } - }); - } - - /// - /// Injects properties using a cached source-generated plan. - /// - private static async Task InjectPropertiesUsingPlanAsync(object instance, PropertyInjectionMetadata[] properties, Dictionary objectBag, MethodMetadata? methodMetadata, TestContextEvents events, ConcurrentDictionary visitedObjects) - { - if (properties.Length == 0) - { - return; - } - - // Process all properties at the same level in parallel - var propertyTasks = properties.Select(metadata => - ProcessPropertyMetadata(instance, metadata, objectBag, methodMetadata, events, visitedObjects, TestContext.Current) - ).ToArray(); - - await Task.WhenAll(propertyTasks); - } - - /// - /// Injects properties using a cached reflection plan. - /// - [UnconditionalSuppressMessage("Trimming", "IL2075:\'this\' argument does not satisfy \'DynamicallyAccessedMembersAttribute\' in call to target method. The return value of the source method does not have matching annotations.")] - private static async Task InjectPropertiesUsingReflectionPlanAsync(object instance, (PropertyInfo Property, IDataSourceAttribute DataSource)[] properties, Dictionary objectBag, MethodMetadata? methodMetadata, TestContextEvents events, ConcurrentDictionary visitedObjects) - { - if (properties.Length == 0) - { - return; - } - - // Process all properties in parallel - var propertyTasks = properties.Select(pair => - ProcessReflectionPropertyDataSource(instance, pair.Property, pair.DataSource, objectBag, methodMetadata, events, visitedObjects, TestContext.Current) - ).ToArray(); - - await Task.WhenAll(propertyTasks); - } - - /// - /// Processes property injection using metadata: creates data source, gets values, and injects them. - /// - [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.")] - private static async Task ProcessPropertyMetadata(object instance, PropertyInjectionMetadata metadata, Dictionary objectBag, MethodMetadata? methodMetadata, - TestContextEvents events, ConcurrentDictionary visitedObjects, TestContext? testContext = null) - { - var dataSource = metadata.CreateDataSource(); - var propertyMetadata = new PropertyMetadata - { - IsStatic = false, - Name = metadata.PropertyName, - ClassMetadata = GetClassMetadataForType(metadata.ContainingType), - Type = metadata.PropertyType, - ReflectionInfo = GetPropertyInfo(metadata.ContainingType, metadata.PropertyName), - Getter = parent => GetPropertyInfo(metadata.ContainingType, metadata.PropertyName).GetValue(parent!)!, - ContainingTypeMetadata = GetClassMetadataForType(metadata.ContainingType) - }; - - // Use centralized factory - var dataGeneratorMetadata = DataGeneratorMetadataCreator.CreateForPropertyInjection( - propertyMetadata, - methodMetadata, - dataSource, - testContext, - testContext?.TestDetails.ClassInstance, - events, - objectBag); - - var dataRows = dataSource.GetDataRowsAsync(dataGeneratorMetadata); - - await foreach (var factory in dataRows) - { - var args = await factory(); - object? value; - - // Handle tuple properties - if the property expects a tuple and we have multiple args, create the tuple - if (args != null && TupleFactory.IsTupleType(metadata.PropertyType)) - { - if (args.Length > 1) - { - // Multiple arguments - create tuple from them - value = TupleFactory.CreateTuple(metadata.PropertyType, args); - } - else if (args.Length == 1 && args[0] != null && TupleFactory.IsTupleType(args[0]!.GetType())) - { - // Single tuple argument - check if it needs type conversion - var tupleValue = args[0]!; - var tupleType = tupleValue.GetType(); - - if (tupleType != metadata.PropertyType) - { - // Tuple types don't match - unwrap and recreate with correct types - var elements = DataSourceHelpers.UnwrapTupleAot(tupleValue); - value = TupleFactory.CreateTuple(metadata.PropertyType, elements); - } - else - { - // Types match - use directly - value = tupleValue; - } - } - else - { - // Single non-tuple argument for tuple property - shouldn't happen but handle gracefully - value = args.FirstOrDefault(); - } - } - else - { - // Non-tuple property - use first argument as before - value = args?.FirstOrDefault(); - } - - // Resolve any Func wrappers - value = await ResolveTestDataValueAsync(metadata.PropertyType, value); - - if (value != null) - { - await ProcessInjectedPropertyValue(instance, value, metadata.SetProperty, objectBag, methodMetadata, events, visitedObjects); - // Add to TestClassInjectedPropertyArguments for tracking - if (testContext != null) - { - testContext.TestDetails.TestClassInjectedPropertyArguments[metadata.PropertyName] = value; - } - break; // Only use first value - } - } - } - - /// - /// Processes a property data source using reflection mode. - /// - [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy \'DynamicallyAccessedMembersAttribute\' in call to target method. The return value of the source method does not have matching annotations.")] - private static async Task ProcessReflectionPropertyDataSource(object instance, PropertyInfo property, IDataSourceAttribute dataSource, Dictionary objectBag, MethodMetadata? methodMetadata, TestContextEvents events, ConcurrentDictionary visitedObjects, TestContext? testContext = null) - { - // Use centralized factory for reflection mode - var dataGeneratorMetadata = DataGeneratorMetadataCreator.CreateForPropertyInjection( - property, - property.DeclaringType!, - methodMetadata, - dataSource, - testContext, - instance, - events, - objectBag); - - var dataRows = dataSource.GetDataRowsAsync(dataGeneratorMetadata); - - await foreach (var factory in dataRows) - { - var args = await factory(); - object? value; - - // Handle tuple properties - if the property expects a tuple and we have multiple args, create the tuple - if (args != null && TupleFactory.IsTupleType(property.PropertyType)) - { - if (args.Length > 1) - { - // Multiple arguments - create tuple from them - value = TupleFactory.CreateTuple(property.PropertyType, args); - } - else if (args.Length == 1 && args[0] != null && TupleFactory.IsTupleType(args[0]!.GetType())) - { - // Single tuple argument - check if it needs type conversion - var tupleValue = args[0]!; - var tupleType = tupleValue.GetType(); - - if (tupleType != property.PropertyType) - { - // Tuple types don't match - unwrap and recreate with correct types - var elements = DataSourceHelpers.UnwrapTupleAot(tupleValue); - value = TupleFactory.CreateTuple(property.PropertyType, elements); - } - else - { - // Types match - use directly - value = tupleValue; - } - } - else - { - // Single non-tuple argument for tuple property - shouldn't happen but handle gracefully - value = args.FirstOrDefault(); - } - } - else - { - // Non-tuple property - use first argument as before - value = args?.FirstOrDefault(); - } - - // Resolve any Func wrappers - value = await ResolveTestDataValueAsync(property.PropertyType, value); - - if (value != null) - { - var setter = CreatePropertySetter(property); - await ProcessInjectedPropertyValue(instance, value, setter, objectBag, methodMetadata, events, visitedObjects); - // Add to TestClassInjectedPropertyArguments for tracking - if (testContext != null) - { - testContext.TestDetails.TestClassInjectedPropertyArguments[property.Name] = value; - } - break; // Only use first value - } - } - } - - /// - /// Processes a single injected property value: tracks it, initializes it, sets it on the instance. - /// - private static async Task ProcessInjectedPropertyValue(object instance, object? propertyValue, Action setProperty, Dictionary objectBag, MethodMetadata? methodMetadata, TestContextEvents events, ConcurrentDictionary visitedObjects) - { - if (propertyValue == null) - { - return; - } - - ObjectTracker.TrackObject(events, propertyValue); - ObjectTracker.TrackOwnership(instance, propertyValue); - - if (ShouldInjectProperties(propertyValue)) - { - await InjectPropertiesIntoObjectAsyncCore(propertyValue, objectBag, methodMetadata, events, visitedObjects); - } - else - { - await ObjectInitializer.InitializeAsync(propertyValue); - } - - setProperty(instance, propertyValue); - } - - /// - /// Resolves Func values by invoking them without using reflection (AOT-safe). - /// - private static ValueTask ResolveTestDataValueAsync(Type type, object? value) - { - if (value == null) - { - return new ValueTask(result: null); - } - - if (value is Delegate del) - { - // Use DynamicInvoke which is AOT-safe for parameterless delegates - var result = del.DynamicInvoke(); - return new ValueTask(result); - } - - return new ValueTask(value); - } - - /// - /// Gets PropertyInfo in an AOT-safe manner. - /// - private static PropertyInfo GetPropertyInfo([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type containingType, string propertyName) - { - return containingType.GetProperty(propertyName)!; - } - - /// - /// Gets or creates ClassMetadata for the specified type. - /// - [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.")] - private static ClassMetadata GetClassMetadataForType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicProperties)] 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}", - TypeReference = new TypeReference { AssemblyQualifiedName = p.ParameterType.AssemblyQualifiedName }, - ReflectionInfo = p - }).ToArray() ?? Array.Empty(); - - return new ClassMetadata - { - Type = type, - TypeReference = TypeReference.CreateConcrete(type.AssemblyQualifiedName ?? type.FullName ?? type.Name), - 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 - }; - }); - } - - // ===================================== - // LEGACY COMPATIBILITY API - // ===================================== - // These methods provide compatibility with the old PropertyInjector API - // while using the unified PropertySourceRegistry internally - - /// - /// Legacy compatibility: Discovers injectable properties for a type - /// - [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Legacy compatibility method")] - public static PropertyInjectionData[] DiscoverInjectableProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)] Type type) - { - return PropertySourceRegistry.DiscoverInjectableProperties(type); - } - - /// - /// Legacy compatibility: Injects properties using array-based data structures - /// - [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Legacy compatibility method")] - public static async Task InjectPropertiesAsync( - TestContext testContext, - object instance, - PropertyDataSource[] propertyDataSources, - PropertyInjectionData[] injectionData, - MethodMetadata testInformation, - string testSessionId) - { - if (instance == null) - { - throw new ArgumentNullException(nameof(instance), "Test instance cannot be null"); - } - - // Use the modern PropertyInjectionService for all injection work - // This ensures consistent behavior and proper recursive injection - var objectBag = new Dictionary(); - - // Process all property data sources in parallel for performance - var propertyTasks = propertyDataSources.Select(propertyDataSource => Task.Run(async () => - { - try - { - // First inject properties into the data source itself (if it has any) - if (ShouldInjectProperties(propertyDataSource.DataSource)) - { - await InjectPropertiesIntoObjectAsync(propertyDataSource.DataSource, objectBag, testInformation, testContext.Events); - } - - // Initialize the data source - await ObjectInitializer.InitializeAsync(propertyDataSource.DataSource); - - // Get the property injection info - var propertyInjection = injectionData.FirstOrDefault(p => p.PropertyName == propertyDataSource.PropertyName); - if (propertyInjection == null) - { - return; // Skip this property - } - - // Create property metadata for the data generator - var propertyMetadata = new PropertyMetadata - { - IsStatic = false, - Name = propertyDataSource.PropertyName, - ClassMetadata = GetClassMetadataForType(testInformation.Type), - Type = propertyInjection.PropertyType, - ReflectionInfo = GetPropertyInfo(testInformation.Type, propertyDataSource.PropertyName), - Getter = parent => GetPropertyInfo(testInformation.Type, propertyDataSource.PropertyName).GetValue(parent!)!, - ContainingTypeMetadata = GetClassMetadataForType(testInformation.Type) - }; - - // Create data generator metadata - var dataGeneratorMetadata = DataGeneratorMetadataCreator.CreateForPropertyInjection( - propertyMetadata, - testInformation, - propertyDataSource.DataSource, - testContext, - instance); - - // Get data rows and process the first one - var dataRows = propertyDataSource.DataSource.GetDataRowsAsync(dataGeneratorMetadata); - await foreach (var factory in dataRows) - { - var args = await factory(); - object? value; - - // Handle tuple properties - need to create tuple from multiple arguments - if (TupleFactory.IsTupleType(propertyInjection.PropertyType)) - { - if (args is { Length: > 1 }) - { - // Multiple arguments - create tuple from them - value = TupleFactory.CreateTuple(propertyInjection.PropertyType, args); - } - else if (args is [not null] && TupleFactory.IsTupleType(args[0]!.GetType())) - { - // Single tuple argument - check if it needs type conversion - var tupleValue = args[0]!; - var tupleType = tupleValue!.GetType(); - - if (tupleType != propertyInjection.PropertyType) - { - // Tuple types don't match - unwrap and recreate with correct types - var elements = DataSourceHelpers.UnwrapTupleAot(tupleValue); - value = TupleFactory.CreateTuple(propertyInjection.PropertyType, elements); - } - else - { - // Types match - use directly - value = tupleValue; - } - } - else - { - // Single non-tuple argument or null - value = args?.FirstOrDefault(); - } - } - else - { - value = args?.FirstOrDefault(); - } - - // Resolve the value (handle Func, Task, etc.) - value = await ResolveTestDataValueAsync(propertyInjection.PropertyType, value); - - if (value != null) - { - // Use the modern service for recursive injection and initialization - // Create a new visited set for this legacy call -#if NETSTANDARD2_0 - var visitedObjects = new ConcurrentDictionary(); -#else - var visitedObjects = new ConcurrentDictionary(System.Collections.Generic.ReferenceEqualityComparer.Instance); -#endif - visitedObjects.TryAdd(instance, 0); // Add the current instance to prevent re-processing - await ProcessInjectedPropertyValue(instance, value, propertyInjection.Setter, objectBag, testInformation, testContext.Events, visitedObjects); - // Add to TestClassInjectedPropertyArguments for tracking - testContext.TestDetails.TestClassInjectedPropertyArguments[propertyInjection.PropertyName] = value; - return; // Only use first value - } - } - } - catch (Exception ex) - { - throw new InvalidOperationException( - $"Failed to resolve data source for property '{propertyDataSource.PropertyName}': {ex.Message}", ex); - } - })).ToArray(); - - await Task.WhenAll(propertyTasks).ConfigureAwait(false); - } - - /// - /// Legacy compatibility: Creates PropertyInjectionData from PropertyInfo - /// - public static PropertyInjectionData CreatePropertyInjection(PropertyInfo property) - { - var setter = CreatePropertySetter(property); - - return new PropertyInjectionData - { - PropertyName = property.Name, - PropertyType = property.PropertyType, - Setter = setter, - ValueFactory = () => throw new InvalidOperationException( - $"Property value factory should be provided by TestDataCombination for {property.Name}") - }; - } - - /// - /// Legacy compatibility: Creates property setter - /// - public static Action CreatePropertySetter(PropertyInfo property) - { - if (property.CanWrite && property.SetMethod != null) - { -#if NETSTANDARD2_0 - return (instance, value) => property.SetValue(instance, value); -#else - var setMethod = property.SetMethod; - var isInitOnly = IsInitOnlyMethod(setMethod); - - if (!isInitOnly) - { - return (instance, value) => property.SetValue(instance, value); - } -#endif - } - - var backingField = GetBackingField(property); - if (backingField != null) - { - return (instance, value) => backingField.SetValue(instance, value); - } - - throw new InvalidOperationException( - $"Property '{property.Name}' on type '{property.DeclaringType?.Name}' " + - $"is not writable and no backing field was found."); - } - - /// - /// Legacy compatibility: Gets backing field for property - /// - [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Legacy compatibility method")] - [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Legacy compatibility method")] - private static FieldInfo? GetBackingField(PropertyInfo property) - { - var declaringType = property.DeclaringType; - if (declaringType == null) - { - return null; - } - - var backingFieldFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy; - - var backingFieldName = $"<{property.Name}>k__BackingField"; - var field = GetFieldSafe(declaringType, backingFieldName, backingFieldFlags); - - if (field != null) - { - return field; - } - - var underscoreName = "_" + char.ToLowerInvariant(property.Name[0]) + property.Name.Substring(1); - field = GetFieldSafe(declaringType, underscoreName, backingFieldFlags); - - if (field != null && field.FieldType == property.PropertyType) - { - return field; - } - - field = GetFieldSafe(declaringType, property.Name, backingFieldFlags); - - if (field != null && field.FieldType == property.PropertyType) - { - return field; - } - return null; - } - /// - /// Helper method to get field with proper trimming suppression - /// - [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Legacy compatibility method")] - private static FieldInfo? GetFieldSafe([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)] Type type, string name, BindingFlags bindingFlags) - { - return type.GetField(name, bindingFlags); - } - /// - /// Legacy compatibility: Checks if method is init-only - /// - [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Legacy compatibility method")] - private static bool IsInitOnlyMethod(MethodInfo setMethod) - { - var methodType = setMethod.GetType(); - var isInitOnlyProperty = methodType.GetProperty("IsInitOnly"); - return isInitOnlyProperty != null && (bool)isInitOnlyProperty.GetValue(setMethod)!; - } } diff --git a/TUnit.Engine/Building/Collectors/AotTestDataCollector.cs b/TUnit.Engine/Building/Collectors/AotTestDataCollector.cs index 4c45cc7259..901403a7fd 100644 --- a/TUnit.Engine/Building/Collectors/AotTestDataCollector.cs +++ b/TUnit.Engine/Building/Collectors/AotTestDataCollector.cs @@ -146,7 +146,7 @@ private Task CreateMetadataFromDynamicDiscoveryResult(DynamicDisco GenericMethodTypeArguments = null, AttributeFactory = () => result.Attributes.ToArray(), #pragma warning disable IL2072 - PropertyInjections = PropertyInjectionService.DiscoverInjectableProperties(result.TestClassType) + PropertyInjections = PropertySourceRegistry.DiscoverInjectableProperties(result.TestClassType) #pragma warning restore IL2072 }); } diff --git a/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs b/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs index 4de56e18a4..e01866e8cb 100644 --- a/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs +++ b/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs @@ -891,7 +891,7 @@ private static Task BuildTestMetadata(Type testClass, MethodInfo t GenericMethodInfo = ReflectionGenericTypeResolver.ExtractGenericMethodInfo(testMethod), GenericMethodTypeArguments = testMethod.IsGenericMethodDefinition ? null : testMethod.GetGenericArguments(), AttributeFactory = () => ReflectionAttributeExtractor.GetAllAttributes(testClass, testMethod), - PropertyInjections = PropertyInjectionService.DiscoverInjectableProperties(testClass), + PropertyInjections = PropertySourceRegistry.DiscoverInjectableProperties(testClass), InheritanceDepth = inheritanceDepth }); } @@ -1839,7 +1839,7 @@ private Task CreateMetadataFromDynamicDiscoveryResult(DynamicDisco GenericMethodInfo = ReflectionGenericTypeResolver.ExtractGenericMethodInfo(methodInfo), GenericMethodTypeArguments = methodInfo.IsGenericMethodDefinition ? null : methodInfo.GetGenericArguments(), AttributeFactory = () => result.Attributes.ToArray(), - PropertyInjections = PropertyInjectionService.DiscoverInjectableProperties(result.TestClassType) + PropertyInjections = PropertySourceRegistry.DiscoverInjectableProperties(result.TestClassType) }; return Task.FromResult(metadata); diff --git a/TUnit.Engine/Services/TestArgumentTrackingService.cs b/TUnit.Engine/Services/TestArgumentTrackingService.cs index 6bd4a8d1be..5bee4a9f56 100644 --- a/TUnit.Engine/Services/TestArgumentTrackingService.cs +++ b/TUnit.Engine/Services/TestArgumentTrackingService.cs @@ -1,4 +1,5 @@ using TUnit.Core; +using TUnit.Core.Initialization; using TUnit.Core.Interfaces; using TUnit.Core.Tracking; @@ -22,33 +23,23 @@ public async ValueTask OnTestRegistered(TestRegisteredContext context) var classArguments = testContext.TestDetails.TestClassArguments; var methodArguments = testContext.TestDetails.TestMethodArguments; - // Inject properties into ClassDataSource instances before tracking - foreach (var classDataItem in classArguments) - { - if (classDataItem != null) - { - await PropertyInjectionService.InjectPropertiesIntoObjectAsync( - classDataItem, - testContext.ObjectBag, - testContext.TestDetails.MethodMetadata, - testContext.Events); - } - } + // Use centralized TestObjectInitializer for all initialization + // Initialize class arguments + await TestObjectInitializer.InitializeArgumentsAsync( + classArguments, + testContext.ObjectBag, + testContext.TestDetails.MethodMetadata, + testContext.Events); - // Also inject properties into MethodDataSource instances - foreach (var methodDataItem in methodArguments) - { - if (methodDataItem != null) - { - await PropertyInjectionService.InjectPropertiesIntoObjectAsync( - methodDataItem, - testContext.ObjectBag, - testContext.TestDetails.MethodMetadata, - testContext.Events); - } - } + // Initialize method arguments + await TestObjectInitializer.InitializeArgumentsAsync( + methodArguments, + testContext.ObjectBag, + testContext.TestDetails.MethodMetadata, + testContext.Events); // Track all constructor and method arguments + // Note: TestObjectInitializer already handles tracking, but we ensure it here for clarity var allArguments = classArguments.Concat(methodArguments); foreach (var obj in allArguments) diff --git a/TUnit.Engine/Services/TestRegistry.cs b/TUnit.Engine/Services/TestRegistry.cs index 88d45f6439..a156a86e8a 100644 --- a/TUnit.Engine/Services/TestRegistry.cs +++ b/TUnit.Engine/Services/TestRegistry.cs @@ -144,7 +144,7 @@ private async Task CreateMetadataFromDynamicDiscoveryResult(Dynami GenericMethodInfo = null, GenericMethodTypeArguments = null, AttributeFactory = () => GetAttributesOptimized(result.Attributes), - PropertyInjections = PropertyInjectionService.DiscoverInjectableProperties(result.TestClassType) + PropertyInjections = PropertySourceRegistry.DiscoverInjectableProperties(result.TestClassType) }); } diff --git a/TUnit.Engine/TestInitializer.cs b/TUnit.Engine/TestInitializer.cs index 7e6e682871..c2db84f934 100644 --- a/TUnit.Engine/TestInitializer.cs +++ b/TUnit.Engine/TestInitializer.cs @@ -1,4 +1,5 @@ using TUnit.Core; +using TUnit.Core.Initialization; using TUnit.Engine.Extensions; using TUnit.Engine.Services; @@ -15,12 +16,10 @@ public TestInitializer(EventReceiverOrchestrator eventReceiverOrchestrator) public async Task InitializeTest(AbstractExecutableTest test, CancellationToken cancellationToken) { - await PropertyInjectionService.InjectPropertiesIntoObjectAsync( + // Use centralized TestObjectInitializer for all initialization + await TestObjectInitializer.InitializeTestClassAsync( test.Context.TestDetails.ClassInstance, - test.Context.ObjectBag, - test.Context.TestDetails.MethodMetadata, - test.Context.Events); - + test.Context); // Initialize and register all eligible objects including event receivers await _eventReceiverOrchestrator.InitializeAllEligibleObjectsAsync(test.Context, cancellationToken).ConfigureAwait(false); From b435fd971daacb6935df045ce8a38701df7f3922 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Fri, 19 Sep 2025 20:19:58 +0100 Subject: [PATCH 4/4] refactor(data-source-initialization): centralize data source initialization logic and remove redundant property injection --- .../CodeGenerators/Writers/AttributeWriter.cs | 104 +------------- .../PropertyInjectionSourceGenerator.cs | 17 ++- .../AsyncDataSourceGeneratorAttribute.cs | 79 ++--------- ...typedDataSourceSourceGeneratorAttribute.cs | 16 +-- .../DataSources/DataSourceInitializer.cs | 111 +++++++++++++++ .../Initialization/TestObjectInitializer.cs | 39 +++--- .../Initialization/PropertyDataResolver.cs | 51 +++++-- .../PropertyInitializationOrchestrator.cs | 30 +++- .../PropertyInitializationPipeline.cs | 16 ++- .../Strategies/NestedPropertyStrategy.cs | 21 ++- .../Strategies/ReflectionPropertyStrategy.cs | 42 ++---- .../SourceGeneratedPropertyStrategy.cs | 42 ++---- .../PropertyValueProcessor.cs | 61 +-------- TUnit.Core/PropertyInjectionService.cs | 32 ++--- TUnit.Core/TestBuilderContext.cs | 5 + TUnit.Engine/Building/TestBuilder.cs | 129 +++++++++++++----- .../Framework/TUnitServiceProvider.cs | 20 ++- .../Services/TestArgumentTrackingService.cs | 11 +- TUnit.Engine/TestInitializer.cs | 6 +- ..._Has_No_API_Changes.DotNet8_0.verified.txt | 26 ++-- ..._Has_No_API_Changes.DotNet9_0.verified.txt | 26 ++-- ...ary_Has_No_API_Changes.Net4_7.verified.txt | 24 ++-- 22 files changed, 469 insertions(+), 439 deletions(-) create mode 100644 TUnit.Core/DataSources/DataSourceInitializer.cs diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Writers/AttributeWriter.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Writers/AttributeWriter.cs index af0117e419..b598bf442f 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Writers/AttributeWriter.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Writers/AttributeWriter.cs @@ -149,8 +149,9 @@ public static string GetAttributeObjectInitializer(Compilation compilation, sourceCodeWriter.Append($"new {attributeName}({formattedConstructorArgs})"); - if (formattedProperties.Length == 0 - && !HasNestedDataGeneratorProperties(attributeData)) + // Only add object initializer if we have regular properties to set + // Don't include data source properties - they'll be handled by property injection + if (formattedProperties.Length == 0) { return sourceCodeWriter.ToString(); } @@ -162,61 +163,11 @@ public static string GetAttributeObjectInitializer(Compilation compilation, sourceCodeWriter.Append($"{property},"); } - WriteDataSourceGeneratorProperties(sourceCodeWriter, compilation, attributeData); - sourceCodeWriter.Append("}"); return sourceCodeWriter.ToString(); } - private static bool HasNestedDataGeneratorProperties(AttributeData attributeData) - { - if (attributeData.AttributeClass is not { } attributeClass) - { - return false; - } - - if (attributeClass.GetMembersIncludingBase().OfType().Any(x => x.GetAttributes().Any(a => a.IsDataSourceAttribute()))) - { - return true; - } - - return false; - } - - private static void WriteDataSourceGeneratorProperties(ICodeWriter sourceCodeWriter, Compilation compilation, AttributeData attributeData) - { - foreach (var propertySymbol in attributeData.AttributeClass?.GetMembers().OfType() ?? []) - { - if (propertySymbol.DeclaredAccessibility != Accessibility.Public) - { - continue; - } - - if (propertySymbol.GetAttributes().FirstOrDefault(x => x.IsDataSourceAttribute()) is not { } dataSourceAttribute) - { - continue; - } - - sourceCodeWriter.Append($"{propertySymbol.Name} = "); - - var propertyType = propertySymbol.Type.GloballyQualified(); - var isNullable = propertySymbol.Type.NullableAnnotation == NullableAnnotation.Annotated; - - if (propertySymbol.Type.IsReferenceType && !isNullable) - { - sourceCodeWriter.Append("null!,"); - } - else if (propertySymbol.Type.IsValueType && !isNullable) - { - sourceCodeWriter.Append($"default({propertyType}),"); - } - else - { - sourceCodeWriter.Append("null,"); - } - } - } private static string FormatConstructorArgument(Compilation compilation, AttributeArgumentSyntax attributeArgumentSyntax) { @@ -253,11 +204,10 @@ public static void WriteAttributeWithoutSyntax(ICodeWriter sourceCodeWriter, Att sourceCodeWriter.Append($"new {attributeName}({formattedConstructorArgs})"); - // Check if we need to add properties (named arguments or data generator properties) + // Check if we need to add properties (named arguments only, not data source properties) var hasNamedArgs = !string.IsNullOrEmpty(formattedNamedArgs); - var hasDataGeneratorProperties = HasNestedDataGeneratorProperties(attributeData); - if (!hasNamedArgs && !hasDataGeneratorProperties) + if (!hasNamedArgs) { return; } @@ -268,55 +218,11 @@ public static void WriteAttributeWithoutSyntax(ICodeWriter sourceCodeWriter, Att if (hasNamedArgs) { sourceCodeWriter.Append($"{formattedNamedArgs}"); - if (hasDataGeneratorProperties) - { - sourceCodeWriter.Append(","); - } - } - - if (hasDataGeneratorProperties) - { - // For attributes without syntax, we still need to handle data generator properties - // but we can't rely on syntax analysis, so we'll use a simpler approach - WriteDataSourceGeneratorPropertiesWithoutSyntax(sourceCodeWriter, attributeData); } sourceCodeWriter.Append("}"); } - private static void WriteDataSourceGeneratorPropertiesWithoutSyntax(ICodeWriter sourceCodeWriter, AttributeData attributeData) - { - foreach (var propertySymbol in attributeData.AttributeClass?.GetMembers().OfType() ?? []) - { - if (propertySymbol.DeclaredAccessibility != Accessibility.Public) - { - continue; - } - - if (propertySymbol.GetAttributes().FirstOrDefault(x => x.IsDataSourceAttribute()) is not { } dataSourceAttribute) - { - continue; - } - - sourceCodeWriter.Append($"{propertySymbol.Name} = "); - - var propertyType = propertySymbol.Type.GloballyQualified(); - var isNullable = propertySymbol.Type.NullableAnnotation == NullableAnnotation.Annotated; - - if (propertySymbol.Type.IsReferenceType && !isNullable) - { - sourceCodeWriter.Append("null!,"); - } - else if (propertySymbol.Type.IsValueType && !isNullable) - { - sourceCodeWriter.Append($"default({propertyType}),"); - } - else - { - sourceCodeWriter.Append("null,"); - } - } - } private static bool ShouldSkipFrameworkSpecificAttribute(Compilation compilation, AttributeData attributeData) { diff --git a/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs b/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs index f89efd1eca..82f50df6b0 100644 --- a/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs @@ -262,6 +262,7 @@ private static void GeneratePropertyMetadata(StringBuilder sb, PropertyWithDataS var propertyType = propInfo.Property.Type.ToDisplayString(); var propertyTypeForTypeof = GetNonNullableTypeString(propInfo.Property.Type); var attributeTypeName = propInfo.DataSourceAttribute.AttributeClass!.ToDisplayString(); + var attributeClass = propInfo.DataSourceAttribute.AttributeClass!; sb.AppendLine(" yield return new PropertyInjectionMetadata"); sb.AppendLine(" {"); @@ -272,7 +273,7 @@ private static void GeneratePropertyMetadata(StringBuilder sb, PropertyWithDataS // Generate CreateDataSource delegate sb.AppendLine(" CreateDataSource = () =>"); sb.AppendLine(" {"); - GenerateDataSourceCreation(sb, propInfo.DataSourceAttribute, attributeTypeName); + GenerateDataSourceCreation(sb, propInfo.DataSourceAttribute, attributeTypeName, attributeClass); sb.AppendLine(" },"); // Generate SetProperty delegate @@ -286,10 +287,14 @@ private static void GeneratePropertyMetadata(StringBuilder sb, PropertyWithDataS sb.AppendLine(); } - private static void GenerateDataSourceCreation(StringBuilder sb, AttributeData attributeData, string attributeTypeName) + private static void GenerateDataSourceCreation(StringBuilder sb, AttributeData attributeData, string attributeTypeName, INamedTypeSymbol attributeClass) { var constructorArgs = string.Join(", ", attributeData.ConstructorArguments.Select(FormatTypedConstant)); + // Check if this is a custom data source that might have its own properties needing injection + // We identify custom data sources as those that inherit from DataSourceGeneratorAttribute or AsyncDataSourceGeneratorAttribute + var isCustomDataSource = IsCustomDataSource(attributeClass); + sb.AppendLine($" var dataSource = new {attributeTypeName}({constructorArgs});"); foreach (var namedArg in attributeData.NamedArguments) @@ -298,8 +303,16 @@ private static void GenerateDataSourceCreation(StringBuilder sb, AttributeData a sb.AppendLine($" dataSource.{namedArg.Key} = {value};"); } + // For custom data sources, we don't initialize them here - that will be handled by DataSourceInitializer + // which is called by PropertyDataResolver.GetInitializedDataSourceAsync sb.AppendLine(" return dataSource;"); } + + private static bool IsCustomDataSource(INamedTypeSymbol attributeClass) + { + // Check if this class implements IDataSourceAttribute + return attributeClass.AllInterfaces.Any(i => i.Name == "IDataSourceAttribute"); + } private static void GeneratePropertySetting(StringBuilder sb, PropertyWithDataSourceAttribute propInfo, string propertyType, string instanceVariableName, string classTypeName) { diff --git a/TUnit.Core/Attributes/TestData/AsyncDataSourceGeneratorAttribute.cs b/TUnit.Core/Attributes/TestData/AsyncDataSourceGeneratorAttribute.cs index 80b3ac163c..4ba8d6f144 100644 --- a/TUnit.Core/Attributes/TestData/AsyncDataSourceGeneratorAttribute.cs +++ b/TUnit.Core/Attributes/TestData/AsyncDataSourceGeneratorAttribute.cs @@ -1,6 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using TUnit.Core.Extensions; -using TUnit.Core.Initialization; namespace TUnit.Core; @@ -11,21 +9,8 @@ public abstract class AsyncDataSourceGeneratorAttribute<[DynamicallyAccessedMemb public sealed override async IAsyncEnumerable>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata) { - // Use centralized TestObjectInitializer for all initialization - // This handles both property injection and object initialization - if (dataGeneratorMetadata is { TestInformation: not null }) - { - await TestObjectInitializer.InitializeAsync(this, - dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag, - dataGeneratorMetadata.TestInformation, - dataGeneratorMetadata.TestBuilderContext.Current.Events); - } - else - { - // Fallback if no context available - await TestObjectInitializer.InitializeAsync(this, TestContext.Current); - } - + // Data source initialization is now handled externally by DataSourceInitializer + // This follows SRP - the attribute is only responsible for generating data, not initialization await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata)) { yield return generateDataSource; @@ -44,19 +29,8 @@ public abstract class AsyncDataSourceGeneratorAttribute< public sealed override async IAsyncEnumerable>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata) { - // Inject properties into the data source attribute itself if we have context - if (dataGeneratorMetadata is { TestInformation: not null }) - { - await TestObjectInitializer.InitializeAsync(this, - dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag, - dataGeneratorMetadata.TestInformation, - dataGeneratorMetadata.TestBuilderContext.Current.Events); - } - else - { - await TestObjectInitializer.InitializeAsync(this, TestContext.Current); - } - + // Data source initialization is now handled externally by DataSourceInitializer + // This follows SRP - the attribute is only responsible for generating data, not initialization await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata)) { yield return generateDataSource; @@ -77,19 +51,8 @@ public abstract class AsyncDataSourceGeneratorAttribute< public sealed override async IAsyncEnumerable>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata) { - // Inject properties into the data source attribute itself if we have context - if (dataGeneratorMetadata is { TestInformation: not null }) - { - await TestObjectInitializer.InitializeAsync(this, - dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag, - dataGeneratorMetadata.TestInformation, - dataGeneratorMetadata.TestBuilderContext.Current.Events); - } - else - { - await TestObjectInitializer.InitializeAsync(this, TestContext.Current); - } - + // Data source initialization is now handled externally by DataSourceInitializer + // This follows SRP - the attribute is only responsible for generating data, not initialization await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata)) { yield return generateDataSource; @@ -112,19 +75,8 @@ public abstract class AsyncDataSourceGeneratorAttribute< public override async IAsyncEnumerable>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata) { - // Inject properties into the data source attribute itself if we have context - if (dataGeneratorMetadata is { TestInformation: not null }) - { - await TestObjectInitializer.InitializeAsync(this, - dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag, - dataGeneratorMetadata.TestInformation, - dataGeneratorMetadata.TestBuilderContext.Current.Events); - } - else - { - await TestObjectInitializer.InitializeAsync(this, TestContext.Current); - } - + // Data source initialization is now handled externally by DataSourceInitializer + // This follows SRP - the attribute is only responsible for generating data, not initialization await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata)) { yield return generateDataSource; @@ -149,19 +101,8 @@ public abstract class AsyncDataSourceGeneratorAttribute< public override async IAsyncEnumerable>> GetTypedDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata) { - // Inject properties into the data source attribute itself if we have context - if (dataGeneratorMetadata is { TestInformation: not null }) - { - await TestObjectInitializer.InitializeAsync(this, - dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag, - dataGeneratorMetadata.TestInformation, - dataGeneratorMetadata.TestBuilderContext.Current.Events); - } - else - { - await TestObjectInitializer.InitializeAsync(this, TestContext.Current); - } - + // Data source initialization is now handled externally by DataSourceInitializer + // This follows SRP - the attribute is only responsible for generating data, not initialization await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata)) { yield return generateDataSource; diff --git a/TUnit.Core/Attributes/TestData/AsyncUntypedDataSourceSourceGeneratorAttribute.cs b/TUnit.Core/Attributes/TestData/AsyncUntypedDataSourceSourceGeneratorAttribute.cs index e505567c21..1d4dcd9b50 100644 --- a/TUnit.Core/Attributes/TestData/AsyncUntypedDataSourceSourceGeneratorAttribute.cs +++ b/TUnit.Core/Attributes/TestData/AsyncUntypedDataSourceSourceGeneratorAttribute.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using TUnit.Core.Initialization; namespace TUnit.Core; @@ -12,19 +11,8 @@ public abstract class AsyncUntypedDataSourceGeneratorAttribute : Attribute, IAsy public async IAsyncEnumerable>> GenerateAsync(DataGeneratorMetadata dataGeneratorMetadata) { - // Use centralized TestObjectInitializer for all initialization - if (dataGeneratorMetadata is { TestInformation: not null }) - { - await TestObjectInitializer.InitializeAsync(this, - dataGeneratorMetadata.TestBuilderContext.Current.ObjectBag, - dataGeneratorMetadata.TestInformation, - dataGeneratorMetadata.TestBuilderContext.Current.Events); - } - else - { - await TestObjectInitializer.InitializeAsync(this, TestContext.Current); - } - + // Data source initialization is now handled externally by DataSourceInitializer + // This follows SRP - the attribute is only responsible for generating data, not initialization await foreach (var generateDataSource in GenerateDataSourcesAsync(dataGeneratorMetadata)) { yield return generateDataSource; diff --git a/TUnit.Core/DataSources/DataSourceInitializer.cs b/TUnit.Core/DataSources/DataSourceInitializer.cs new file mode 100644 index 0000000000..1a19dea339 --- /dev/null +++ b/TUnit.Core/DataSources/DataSourceInitializer.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using TUnit.Core.Initialization; +using TUnit.Core.Interfaces; +using TUnit.Core.PropertyInjection; +using TUnit.Core.Tracking; + +namespace TUnit.Core.DataSources; + +/// +/// Centralized service responsible for initializing data source instances. +/// Ensures all data sources are properly initialized before use, regardless of where they're used +/// (properties, constructor arguments, or method arguments). +/// +internal sealed class DataSourceInitializer +{ + private readonly Dictionary _initializationTasks = new(); + private readonly object _lock = new(); + private PropertyInjectionService? _propertyInjectionService; + + public void Initialize(PropertyInjectionService propertyInjectionService) + { + _propertyInjectionService = propertyInjectionService; + } + + /// + /// Ensures a data source instance is fully initialized before use. + /// This includes property injection and calling IAsyncInitializer if implemented. + /// + public async Task EnsureInitializedAsync( + T dataSource, + Dictionary? objectBag = null, + MethodMetadata? methodMetadata = null, + TestContextEvents? events = null) where T : notnull + { + if (dataSource == null) + { + throw new ArgumentNullException(nameof(dataSource)); + } + + // Check if already initialized or being initialized + Task? existingTask; + lock (_lock) + { + if (_initializationTasks.TryGetValue(dataSource, out existingTask)) + { + // Already initialized or being initialized + } + else + { + // Start initialization + existingTask = InitializeDataSourceAsync(dataSource, objectBag, methodMetadata, events); + _initializationTasks[dataSource] = existingTask; + } + } + + await existingTask; + return dataSource; + } + + /// + /// Initializes a data source instance with the complete lifecycle. + /// + private async Task InitializeDataSourceAsync( + object dataSource, + Dictionary? objectBag, + MethodMetadata? methodMetadata, + TestContextEvents? events) + { + try + { + // Initialize the data source directly here + // Step 1: Property injection - use PropertyInjectionService if available + if (_propertyInjectionService != null && PropertyInjectionCache.HasInjectableProperties(dataSource.GetType())) + { + await _propertyInjectionService.InjectPropertiesIntoObjectAsync( + dataSource, objectBag, methodMetadata, events); + } + + // Step 2: IAsyncInitializer + if (dataSource is IAsyncInitializer asyncInitializer) + { + await asyncInitializer.InitializeAsync(); + } + + // Step 3: Track for disposal + if (events != null) + { + ObjectTracker.TrackObject(events, dataSource); + } + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Failed to initialize data source of type '{dataSource.GetType().Name}': {ex.Message}", ex); + } + } + + /// + /// Clears the initialization cache. Should be called at the end of test sessions. + /// + public void ClearCache() + { + lock (_lock) + { + _initializationTasks.Clear(); + } + } +} \ No newline at end of file diff --git a/TUnit.Core/Initialization/TestObjectInitializer.cs b/TUnit.Core/Initialization/TestObjectInitializer.cs index 54780311b9..941c175431 100644 --- a/TUnit.Core/Initialization/TestObjectInitializer.cs +++ b/TUnit.Core/Initialization/TestObjectInitializer.cs @@ -11,13 +11,20 @@ namespace TUnit.Core.Initialization; /// Centralized service for initializing test-related objects. /// Provides a single entry point for the complete object initialization lifecycle. /// -public static class TestObjectInitializer +internal sealed class TestObjectInitializer { + private readonly PropertyInjectionService _propertyInjectionService; + + public TestObjectInitializer(PropertyInjectionService propertyInjectionService) + { + _propertyInjectionService = propertyInjectionService ?? throw new ArgumentNullException(nameof(propertyInjectionService)); + } + /// /// Initializes a single object with the complete lifecycle: /// Create → Inject Properties → Initialize → Track → Ready /// - public static async Task InitializeAsync( + public async Task InitializeAsync( T instance, TestContext? testContext = null) where T : notnull { @@ -34,7 +41,7 @@ public static async Task InitializeAsync( /// /// Initializes a single object with explicit context parameters. /// - public static async Task InitializeAsync( + public async Task InitializeAsync( object instance, Dictionary? objectBag = null, MethodMetadata? methodMetadata = null, @@ -59,7 +66,7 @@ public static async Task InitializeAsync( /// /// Initializes multiple objects (e.g., test arguments) in parallel. /// - public static async Task InitializeArgumentsAsync( + public async Task InitializeArgumentsAsync( object?[] arguments, Dictionary objectBag, MethodMetadata methodMetadata, @@ -94,7 +101,7 @@ public static async Task InitializeArgumentsAsync( /// /// Initializes test class instance with full lifecycle. /// - public static async Task InitializeTestClassAsync( + public async Task InitializeTestClassAsync( object testClassInstance, TestContext testContext) { @@ -115,14 +122,14 @@ public static async Task InitializeTestClassAsync( /// /// Core initialization logic - the single place where all initialization happens. /// - private static async Task InitializeObjectAsync(object instance, InitializationContext context) + private async Task InitializeObjectAsync(object instance, InitializationContext context) { try { // Step 1: Property Injection if (RequiresPropertyInjection(instance)) { - await PropertyInjectionService.InjectPropertiesIntoObjectAsync( + await _propertyInjectionService.InjectPropertiesIntoObjectAsync( instance, context.ObjectBag, context.MethodMetadata, @@ -151,7 +158,7 @@ await PropertyInjectionService.InjectPropertiesIntoObjectAsync( /// /// Determines if an object requires property injection. /// - private static bool RequiresPropertyInjection(object instance) + private bool RequiresPropertyInjection(object instance) { // Use the existing cache from PropertyInjectionCache return PropertyInjection.PropertyInjectionCache.HasInjectableProperties(instance.GetType()); @@ -160,7 +167,7 @@ private static bool RequiresPropertyInjection(object instance) /// /// Tracks an object for disposal and ownership. /// - private static void TrackObject(object instance, InitializationContext context) + private void TrackObject(object instance, InitializationContext context) { // Only track if we have events context if (context.Events != null) @@ -172,7 +179,7 @@ private static void TrackObject(object instance, InitializationContext context) /// /// Hook for post-initialization processing. /// - private static Task OnObjectInitializedAsync(object instance, InitializationContext context) + private Task OnObjectInitializedAsync(object instance, InitializationContext context) { // Extension point for future features (e.g., validation, logging) return Task.CompletedTask; @@ -181,7 +188,7 @@ private static Task OnObjectInitializedAsync(object instance, InitializationCont /// /// Prepares initialization context from test context. /// - private static InitializationContext PrepareContext(TestContext? testContext) + private InitializationContext PrepareContext(TestContext? testContext) { return new InitializationContext { @@ -197,10 +204,10 @@ private static InitializationContext PrepareContext(TestContext? testContext) /// private class InitializationContext { - public required Dictionary ObjectBag { get; init; } - public MethodMetadata? MethodMetadata { get; init; } - public required TestContextEvents Events { get; init; } - public TestContext? TestContext { get; init; } + public Dictionary ObjectBag { get; set; } = null!; + public MethodMetadata? MethodMetadata { get; set; } + public TestContextEvents Events { get; set; } = null!; + public TestContext? TestContext { get; set; } } } @@ -213,7 +220,7 @@ public TestObjectInitializationException(string message) : base(message) { } - public TestObjectInitializationException(string message, Exception innerException) + public TestObjectInitializationException(string message, Exception innerException) : base(message, innerException) { } diff --git a/TUnit.Core/PropertyInjection/Initialization/PropertyDataResolver.cs b/TUnit.Core/PropertyInjection/Initialization/PropertyDataResolver.cs index ef0f0a8e40..b0ede06961 100644 --- a/TUnit.Core/PropertyInjection/Initialization/PropertyDataResolver.cs +++ b/TUnit.Core/PropertyInjection/Initialization/PropertyDataResolver.cs @@ -2,6 +2,9 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; +using TUnit.Core.DataSources; +using TUnit.Core.Initialization; +using TUnit.Core.Interfaces; using TUnit.Core.Interfaces.SourceGenerator; namespace TUnit.Core.PropertyInjection.Initialization; @@ -16,9 +19,9 @@ internal static class PropertyDataResolver /// Resolves data from a property's data source. /// [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Property types handled dynamically")] - public static async Task ResolvePropertyDataAsync(PropertyInitializationContext context) + public static async Task ResolvePropertyDataAsync(PropertyInitializationContext context, DataSourceInitializer dataSourceInitializer, TestObjectInitializer testObjectInitializer) { - var dataSource = GetDataSource(context); + var dataSource = await GetInitializedDataSourceAsync(context, dataSourceInitializer); if (dataSource == null) { return null; @@ -36,8 +39,26 @@ internal static class PropertyDataResolver // Resolve any Func wrappers value = await ResolveDelegateValue(value); + // Initialize the resolved value if needed if (value != null) { + // If the resolved value is itself a data source, ensure it's initialized + if (value is IDataSourceAttribute dataSourceValue) + { + value = await dataSourceInitializer.EnsureInitializedAsync( + dataSourceValue, + context.ObjectBag, + context.MethodMetadata, + context.Events); + } + // Otherwise, initialize if it has injectable properties or implements IAsyncInitializer + else if (PropertyInjectionCache.HasInjectableProperties(value.GetType()) || + value is IAsyncInitializer) + { + // Use TestObjectInitializer for complete initialization + value = await testObjectInitializer.InitializeAsync(value, context.TestContext); + } + return value; } } @@ -46,21 +67,35 @@ internal static class PropertyDataResolver } /// - /// Gets the data source from the context. + /// Gets an initialized data source from the context. + /// Ensures the data source is fully initialized (including property injection) before returning it. /// - private static IDataSourceAttribute? GetDataSource(PropertyInitializationContext context) + private static async Task GetInitializedDataSourceAsync(PropertyInitializationContext context, DataSourceInitializer dataSourceInitializer) { + IDataSourceAttribute? dataSource = null; + if (context.DataSource != null) { - return context.DataSource; + dataSource = context.DataSource; + } + else if (context.SourceGeneratedMetadata != null) + { + // Create a new data source instance + dataSource = context.SourceGeneratedMetadata.CreateDataSource(); } - if (context.SourceGeneratedMetadata != null) + if (dataSource == null) { - return context.SourceGeneratedMetadata.CreateDataSource(); + return null; } - return null; + // Ensure the data source is fully initialized before use + // This handles property injection and IAsyncInitializer + return await dataSourceInitializer.EnsureInitializedAsync( + dataSource, + context.ObjectBag, + context.MethodMetadata, + context.Events); } /// diff --git a/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationOrchestrator.cs b/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationOrchestrator.cs index 0827977521..facdf2d04a 100644 --- a/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationOrchestrator.cs +++ b/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationOrchestrator.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; +using TUnit.Core.DataSources; +using TUnit.Core.Initialization; using TUnit.Core.Interfaces.SourceGenerator; namespace TUnit.Core.PropertyInjection.Initialization; @@ -15,11 +17,28 @@ namespace TUnit.Core.PropertyInjection.Initialization; /// internal sealed class PropertyInitializationOrchestrator { - private readonly PropertyInitializationPipeline _pipeline; + private PropertyInitializationPipeline _pipeline; + internal DataSourceInitializer DataSourceInitializer { get; } + internal TestObjectInitializer TestObjectInitializer { get; private set; } - public PropertyInitializationOrchestrator() + public PropertyInitializationOrchestrator(DataSourceInitializer dataSourceInitializer, TestObjectInitializer? testObjectInitializer) { - _pipeline = PropertyInitializationPipeline.CreateDefault(); + DataSourceInitializer = dataSourceInitializer ?? throw new ArgumentNullException(nameof(dataSourceInitializer)); + TestObjectInitializer = testObjectInitializer!; // Will be set via Initialize() + if (testObjectInitializer != null) + { + _pipeline = PropertyInitializationPipeline.CreateDefault(dataSourceInitializer, testObjectInitializer); + } + else + { + _pipeline = null!; // Will be set via Initialize() + } + } + + public void Initialize(TestObjectInitializer testObjectInitializer) + { + TestObjectInitializer = testObjectInitializer ?? throw new ArgumentNullException(nameof(testObjectInitializer)); + _pipeline = PropertyInitializationPipeline.CreateDefault(DataSourceInitializer, testObjectInitializer); } /// @@ -86,6 +105,7 @@ public async Task InitializeObjectWithPropertiesAsync( } // Initialize properties based on the mode + // Properties will be fully initialized (including nested initialization) by the strategies if (SourceRegistrar.IsEnabled) { await InitializePropertiesAsync( @@ -97,7 +117,8 @@ await InitializePropertiesAsync( instance, plan.ReflectionProperties, objectBag, methodMetadata, events, visitedObjects); } - // Initialize the object itself after properties are set + // Initialize the object itself after all its properties are fully initialized + // This ensures properties are available when IAsyncInitializer.InitializeAsync() is called await ObjectInitializer.InitializeAsync(instance); } @@ -162,5 +183,4 @@ private PropertyInitializationContext CreateContext( /// /// Gets the singleton instance of the orchestrator. /// - public static PropertyInitializationOrchestrator Instance { get; } = new(); } \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationPipeline.cs b/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationPipeline.cs index 0b64bf2255..a505c6f9a7 100644 --- a/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationPipeline.cs +++ b/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationPipeline.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using TUnit.Core.DataSources; +using TUnit.Core.Initialization; using TUnit.Core.PropertyInjection.Initialization.Strategies; namespace TUnit.Core.PropertyInjection.Initialization; @@ -14,14 +16,16 @@ internal sealed class PropertyInitializationPipeline private readonly List _strategies; private readonly List> _beforeSteps; private readonly List> _afterSteps; + private readonly DataSourceInitializer _dataSourceInitializer; - public PropertyInitializationPipeline() + public PropertyInitializationPipeline(DataSourceInitializer dataSourceInitializer, TestObjectInitializer testObjectInitializer) { + _dataSourceInitializer = dataSourceInitializer ?? throw new ArgumentNullException(nameof(dataSourceInitializer)); _strategies = new List { - new SourceGeneratedPropertyStrategy(), - new ReflectionPropertyStrategy(), - new NestedPropertyStrategy() + new SourceGeneratedPropertyStrategy(dataSourceInitializer, testObjectInitializer), + new ReflectionPropertyStrategy(dataSourceInitializer, testObjectInitializer), + new NestedPropertyStrategy(dataSourceInitializer, testObjectInitializer) }; _beforeSteps = new List>(); @@ -113,9 +117,9 @@ public async Task ExecuteParallelAsync(IEnumerable /// Creates a default pipeline with standard steps. /// - public static PropertyInitializationPipeline CreateDefault() + public static PropertyInitializationPipeline CreateDefault(DataSourceInitializer dataSourceInitializer, TestObjectInitializer testObjectInitializer) { - return new PropertyInitializationPipeline() + return new PropertyInitializationPipeline(dataSourceInitializer, testObjectInitializer) .AddBeforeStep(ValidateContext) .AddAfterStep(FinalizeInitialization); } diff --git a/TUnit.Core/PropertyInjection/Initialization/Strategies/NestedPropertyStrategy.cs b/TUnit.Core/PropertyInjection/Initialization/Strategies/NestedPropertyStrategy.cs index afb4abd271..81aa1d70e2 100644 --- a/TUnit.Core/PropertyInjection/Initialization/Strategies/NestedPropertyStrategy.cs +++ b/TUnit.Core/PropertyInjection/Initialization/Strategies/NestedPropertyStrategy.cs @@ -1,4 +1,6 @@ using System.Threading.Tasks; +using TUnit.Core.DataSources; +using TUnit.Core.Initialization; namespace TUnit.Core.PropertyInjection.Initialization.Strategies; @@ -8,6 +10,21 @@ namespace TUnit.Core.PropertyInjection.Initialization.Strategies; /// internal sealed class NestedPropertyStrategy : IPropertyInitializationStrategy { + private readonly DataSourceInitializer _dataSourceInitializer; + private readonly TestObjectInitializer _testObjectInitializer; + + public NestedPropertyStrategy(DataSourceInitializer dataSourceInitializer, TestObjectInitializer testObjectInitializer) + { + _dataSourceInitializer = dataSourceInitializer ?? throw new System.ArgumentNullException(nameof(dataSourceInitializer)); + _testObjectInitializer = testObjectInitializer ?? throw new System.ArgumentNullException(nameof(testObjectInitializer)); + } + + public NestedPropertyStrategy() + { + // Default constructor for backward compatibility if needed + _dataSourceInitializer = null!; + _testObjectInitializer = null!; + } /// /// Determines if this strategy can handle nested properties. /// @@ -73,7 +90,7 @@ private async Task ProcessSourceGeneratedNestedProperties( var tasks = plan.SourceGeneratedProperties.Select(async metadata => { var nestedContext = CreateNestedContext(parentContext, instance, metadata); - var strategy = new SourceGeneratedPropertyStrategy(); + var strategy = new SourceGeneratedPropertyStrategy(_dataSourceInitializer, _testObjectInitializer); if (strategy.CanHandle(nestedContext)) { @@ -95,7 +112,7 @@ private async Task ProcessReflectionNestedProperties( var tasks = plan.ReflectionProperties.Select(async pair => { var nestedContext = CreateNestedContext(parentContext, instance, pair.Property, pair.DataSource); - var strategy = new ReflectionPropertyStrategy(); + var strategy = new ReflectionPropertyStrategy(_dataSourceInitializer, _testObjectInitializer); if (strategy.CanHandle(nestedContext)) { diff --git a/TUnit.Core/PropertyInjection/Initialization/Strategies/ReflectionPropertyStrategy.cs b/TUnit.Core/PropertyInjection/Initialization/Strategies/ReflectionPropertyStrategy.cs index 58501ed984..f5028337d2 100644 --- a/TUnit.Core/PropertyInjection/Initialization/Strategies/ReflectionPropertyStrategy.cs +++ b/TUnit.Core/PropertyInjection/Initialization/Strategies/ReflectionPropertyStrategy.cs @@ -1,5 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; +using TUnit.Core.DataSources; +using TUnit.Core.Initialization; namespace TUnit.Core.PropertyInjection.Initialization.Strategies; @@ -9,6 +11,14 @@ namespace TUnit.Core.PropertyInjection.Initialization.Strategies; /// internal sealed class ReflectionPropertyStrategy : IPropertyInitializationStrategy { + private readonly DataSourceInitializer _dataSourceInitializer; + private readonly TestObjectInitializer _testObjectInitializer; + + public ReflectionPropertyStrategy(DataSourceInitializer dataSourceInitializer, TestObjectInitializer testObjectInitializer) + { + _dataSourceInitializer = dataSourceInitializer ?? throw new System.ArgumentNullException(nameof(dataSourceInitializer)); + _testObjectInitializer = testObjectInitializer ?? throw new System.ArgumentNullException(nameof(testObjectInitializer)); + } /// /// Determines if this strategy can handle reflection-based properties. /// @@ -29,7 +39,7 @@ public async Task InitializePropertyAsync(PropertyInitializationContext context) } // Step 1: Resolve data from the data source - var resolvedValue = await PropertyDataResolver.ResolvePropertyDataAsync(context); + var resolvedValue = await PropertyDataResolver.ResolvePropertyDataAsync(context, _dataSourceInitializer, _testObjectInitializer); if (resolvedValue == null) { return; @@ -40,36 +50,12 @@ public async Task InitializePropertyAsync(PropertyInitializationContext context) // Step 2: Track the property value PropertyTrackingService.TrackPropertyValue(context, resolvedValue); - // Step 3: Handle nested property initialization - if (PropertyInjectionCache.HasInjectableProperties(resolvedValue.GetType())) - { - // Mark for recursive processing - await InitializeNestedProperties(context, resolvedValue); - } - else - { - // Just initialize the object - await ObjectInitializer.InitializeAsync(resolvedValue); - } - - // Step 4: Set the property value + // Step 3: Set the property value + // The value has already been initialized by PropertyDataResolver if needed context.PropertySetter(context.Instance, resolvedValue); - // Step 5: Add to test context tracking + // Step 4: Add to test context tracking PropertyTrackingService.AddToTestContext(context, resolvedValue); } - /// - /// Handles initialization of nested properties. - /// - private async Task InitializeNestedProperties(PropertyInitializationContext context, object propertyValue) - { - // This will be handled by the PropertyInjectionService recursively - // We just need to ensure it's initialized - await PropertyInjectionService.InjectPropertiesIntoObjectAsync( - propertyValue, - context.ObjectBag, - context.MethodMetadata, - context.Events); - } } \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/Initialization/Strategies/SourceGeneratedPropertyStrategy.cs b/TUnit.Core/PropertyInjection/Initialization/Strategies/SourceGeneratedPropertyStrategy.cs index 12a17eb585..3c78563702 100644 --- a/TUnit.Core/PropertyInjection/Initialization/Strategies/SourceGeneratedPropertyStrategy.cs +++ b/TUnit.Core/PropertyInjection/Initialization/Strategies/SourceGeneratedPropertyStrategy.cs @@ -1,4 +1,6 @@ using System.Threading.Tasks; +using TUnit.Core.DataSources; +using TUnit.Core.Initialization; namespace TUnit.Core.PropertyInjection.Initialization.Strategies; @@ -8,6 +10,14 @@ namespace TUnit.Core.PropertyInjection.Initialization.Strategies; /// internal sealed class SourceGeneratedPropertyStrategy : IPropertyInitializationStrategy { + private readonly DataSourceInitializer _dataSourceInitializer; + private readonly TestObjectInitializer _testObjectInitializer; + + public SourceGeneratedPropertyStrategy(DataSourceInitializer dataSourceInitializer, TestObjectInitializer testObjectInitializer) + { + _dataSourceInitializer = dataSourceInitializer ?? throw new System.ArgumentNullException(nameof(dataSourceInitializer)); + _testObjectInitializer = testObjectInitializer ?? throw new System.ArgumentNullException(nameof(testObjectInitializer)); + } /// /// Determines if this strategy can handle source-generated properties. /// @@ -27,7 +37,7 @@ public async Task InitializePropertyAsync(PropertyInitializationContext context) } // Step 1: Resolve data from the data source - var resolvedValue = await PropertyDataResolver.ResolvePropertyDataAsync(context); + var resolvedValue = await PropertyDataResolver.ResolvePropertyDataAsync(context, _dataSourceInitializer, _testObjectInitializer); if (resolvedValue == null) { return; @@ -38,36 +48,12 @@ public async Task InitializePropertyAsync(PropertyInitializationContext context) // Step 2: Track the property value PropertyTrackingService.TrackPropertyValue(context, resolvedValue); - // Step 3: Handle nested property initialization - if (PropertyInjectionCache.HasInjectableProperties(resolvedValue.GetType())) - { - // Mark for recursive processing - await InitializeNestedProperties(context, resolvedValue); - } - else - { - // Just initialize the object - await ObjectInitializer.InitializeAsync(resolvedValue); - } - - // Step 4: Set the property value + // Step 3: Set the property value + // The value has already been initialized by PropertyDataResolver if needed context.SourceGeneratedMetadata.SetProperty(context.Instance, resolvedValue); - // Step 5: Add to test context tracking + // Step 4: Add to test context tracking PropertyTrackingService.AddToTestContext(context, resolvedValue); } - /// - /// Handles initialization of nested properties. - /// - private async Task InitializeNestedProperties(PropertyInitializationContext context, object propertyValue) - { - // This will be handled by the PropertyInjectionService recursively - // We just need to ensure it's initialized - await PropertyInjectionService.InjectPropertiesIntoObjectAsync( - propertyValue, - context.ObjectBag, - context.MethodMetadata, - context.Events); - } } \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/PropertyValueProcessor.cs b/TUnit.Core/PropertyInjection/PropertyValueProcessor.cs index 2dbeab5a41..080a365457 100644 --- a/TUnit.Core/PropertyInjection/PropertyValueProcessor.cs +++ b/TUnit.Core/PropertyInjection/PropertyValueProcessor.cs @@ -11,50 +11,8 @@ namespace TUnit.Core.PropertyInjection; /// Processes property values during injection. /// Handles value resolution, tracking, and recursive injection. /// -internal sealed class PropertyValueProcessor +internal static class PropertyValueProcessor { - public PropertyValueProcessor() - { - } - - /// - /// Processes a single injected property value: tracks it, initializes it, sets it on the instance. - /// - public async Task ProcessInjectedValueAsync( - object instance, - object? propertyValue, - Action setProperty, - Dictionary objectBag, - MethodMetadata? methodMetadata, - TestContextEvents events, - ConcurrentDictionary visitedObjects) - { - if (propertyValue == null) - { - return; - } - - // Track the object for disposal - ObjectTracker.TrackObject(events, propertyValue); - ObjectTracker.TrackOwnership(instance, propertyValue); - - // Check if the property value itself needs property injection - if (ShouldInjectProperties(propertyValue)) - { - // Recursively inject properties into the property value - await PropertyInjectionService.InjectPropertiesIntoObjectAsync( - propertyValue, objectBag, methodMetadata, events); - } - else - { - // Just initialize the object - await ObjectInitializer.InitializeAsync(propertyValue); - } - - // Set the property value on the instance - setProperty(instance, propertyValue); - } - /// /// Resolves Func values by invoking them without using reflection (AOT-safe). /// @@ -74,21 +32,4 @@ await PropertyInjectionService.InjectPropertiesIntoObjectAsync( return new ValueTask(value); } - - /// - /// Determines if an object should have properties injected based on whether it has properties with data source attributes. - /// - private static bool ShouldInjectProperties(object? obj) - { - if (obj == null) - { - return false; - } - - var type = obj.GetType(); - - // Check if this type has any injectable properties - // This will use cached results from PropertyInjectionService - return PropertyInjectionCache.HasInjectableProperties(type); - } } \ No newline at end of file diff --git a/TUnit.Core/PropertyInjectionService.cs b/TUnit.Core/PropertyInjectionService.cs index 33199ef79b..4ae79f8cf7 100644 --- a/TUnit.Core/PropertyInjectionService.cs +++ b/TUnit.Core/PropertyInjectionService.cs @@ -2,6 +2,8 @@ using TUnit.Core.Tracking; using System.Diagnostics.CodeAnalysis; using TUnit.Core.Data; +using TUnit.Core.DataSources; +using TUnit.Core.Initialization; using TUnit.Core.Interfaces.SourceGenerator; using TUnit.Core.Enums; using TUnit.Core.Services; @@ -19,30 +21,25 @@ namespace TUnit.Core; /// internal sealed class PropertyInjectionService { - private static readonly PropertyInjectionService _instance = new(); private readonly PropertyInitializationOrchestrator _orchestrator; - public PropertyInjectionService() + public PropertyInjectionService(DataSourceInitializer dataSourceInitializer) { - _orchestrator = PropertyInitializationOrchestrator.Instance; + // We'll set TestObjectInitializer later to break the circular dependency + _orchestrator = new PropertyInitializationOrchestrator(dataSourceInitializer, null!); + } + + public void Initialize(TestObjectInitializer testObjectInitializer) + { + _orchestrator.Initialize(testObjectInitializer); } - - /// - /// Gets the singleton instance of the PropertyInjectionService. - /// - public static PropertyInjectionService Instance => _instance; /// /// Injects properties with data sources into argument objects just before test execution. /// This ensures properties are only initialized when the test is about to run. /// Arguments are processed in parallel for better performance. /// - public static Task InjectPropertiesIntoArgumentsAsync(object?[] arguments, Dictionary objectBag, MethodMetadata methodMetadata, TestContextEvents events) - { - return _instance.InjectPropertiesIntoArgumentsAsyncCore(arguments, objectBag, methodMetadata, events); - } - - private async Task InjectPropertiesIntoArgumentsAsyncCore(object?[] arguments, Dictionary objectBag, MethodMetadata methodMetadata, TestContextEvents events) + public async Task InjectPropertiesIntoArgumentsAsync(object?[] arguments, Dictionary objectBag, MethodMetadata methodMetadata, TestContextEvents events) { if (arguments.Length == 0) { @@ -73,12 +70,7 @@ private async Task InjectPropertiesIntoArgumentsAsyncCore(object?[] arguments, D /// Uses source generation mode when available, falls back to reflection mode. /// After injection, handles tracking, initialization, and recursive injection. /// - public static Task InjectPropertiesIntoObjectAsync(object instance, Dictionary? objectBag, MethodMetadata? methodMetadata, TestContextEvents? events) - { - return _instance.InjectPropertiesIntoObjectAsyncInstance(instance, objectBag, methodMetadata, events); - } - - private Task InjectPropertiesIntoObjectAsyncInstance(object instance, Dictionary? objectBag, MethodMetadata? methodMetadata, TestContextEvents? events) + public Task InjectPropertiesIntoObjectAsync(object instance, Dictionary? objectBag, MethodMetadata? methodMetadata, TestContextEvents? events) { // Start with an empty visited set for cycle detection #if NETSTANDARD2_0 diff --git a/TUnit.Core/TestBuilderContext.cs b/TUnit.Core/TestBuilderContext.cs index 086a488993..c80504217d 100644 --- a/TUnit.Core/TestBuilderContext.cs +++ b/TUnit.Core/TestBuilderContext.cs @@ -30,6 +30,11 @@ public static TestBuilderContext? Current public required MethodMetadata TestMetadata { get; init; } internal IClassConstructor? ClassConstructor { get; set; } + + /// + /// Cached and initialized attributes for the test + /// + internal Attribute[]? InitializedAttributes { get; set; } public void RegisterForInitialization(object? obj) { diff --git a/TUnit.Engine/Building/TestBuilder.cs b/TUnit.Engine/Building/TestBuilder.cs index 2a1b94b406..874d2b8835 100644 --- a/TUnit.Engine/Building/TestBuilder.cs +++ b/TUnit.Engine/Building/TestBuilder.cs @@ -1,4 +1,5 @@ using TUnit.Core; +using TUnit.Core.DataSources; using TUnit.Core.Enums; using TUnit.Core.Exceptions; using TUnit.Core.Helpers; @@ -16,18 +17,28 @@ internal sealed class TestBuilder : ITestBuilder private readonly string _sessionId; private readonly EventReceiverOrchestrator _eventReceiverOrchestrator; private readonly IContextProvider _contextProvider; - - public TestBuilder(string sessionId, EventReceiverOrchestrator eventReceiverOrchestrator, IContextProvider contextProvider) + private readonly PropertyInjectionService _propertyInjectionService; + private readonly DataSourceInitializer _dataSourceInitializer; + + public TestBuilder( + string sessionId, + EventReceiverOrchestrator eventReceiverOrchestrator, + IContextProvider contextProvider, + PropertyInjectionService propertyInjectionService, + DataSourceInitializer dataSourceInitializer) { _sessionId = sessionId; _eventReceiverOrchestrator = eventReceiverOrchestrator; _contextProvider = contextProvider; + _propertyInjectionService = propertyInjectionService; + _dataSourceInitializer = dataSourceInitializer; } private async Task CreateInstance(TestMetadata metadata, Type[] resolvedClassGenericArgs, object?[] classData, TestBuilderContext builderContext) { // First try to create instance with ClassConstructor attribute - var attributes = metadata.AttributeFactory(); + // Use attributes from context if available + var attributes = builderContext.InitializedAttributes ?? metadata.AttributeFactory(); var instance = await ClassConstructorHelper.TryCreateInstanceWithClassConstructor( attributes, @@ -87,8 +98,8 @@ public async Task> BuildTestsFromMetadataAsy } - // Extract repeat count from attributes - var attributes = metadata.AttributeFactory.Invoke(); + // Create and initialize attributes ONCE + var attributes = await InitializeAttributesAsync(metadata.AttributeFactory.Invoke()); var filteredAttributes = ScopedAttributeFilter.FilterScopedAttributes(attributes); var repeatAttr = filteredAttributes.OfType().FirstOrDefault(); var repeatCount = repeatAttr?.Times ?? 0; @@ -105,12 +116,12 @@ public async Task> BuildTestsFromMetadataAsy { TestMetadata = metadata.MethodMetadata, Events = new TestContextEvents(), - ObjectBag = new Dictionary() + ObjectBag = new Dictionary(), + InitializedAttributes = attributes // Store the initialized attributes }; - // Check for ClassConstructor attribute and set it early if present - var classAttributes = metadata.AttributeFactory(); - var classConstructorAttribute = classAttributes.OfType().FirstOrDefault(); + // Check for ClassConstructor attribute and set it early if present (reuse already created attributes) + var classConstructorAttribute = attributes.OfType().FirstOrDefault(); if (classConstructorAttribute != null) { testBuilderContext.ClassConstructor = (IClassConstructor)Activator.CreateInstance(classConstructorAttribute.ClassConstructorType)!; @@ -119,12 +130,13 @@ public async Task> BuildTestsFromMetadataAsy var contextAccessor = new TestBuilderContextAccessor(testBuilderContext); var classDataAttributeIndex = 0; - foreach (var classDataSource in GetDataSources(metadata.ClassDataSources)) + foreach (var classDataSource in await GetDataSourcesAsync(metadata.ClassDataSources)) { classDataAttributeIndex++; var classDataLoopIndex = 0; - await foreach (var classDataFactory in classDataSource.GetDataRowsAsync( + await foreach (var classDataFactory in GetInitializedDataRowsAsync( + classDataSource, DataGeneratorMetadataCreator.CreateDataGeneratorMetadata ( testMetadata: metadata, @@ -190,12 +202,13 @@ public async Task> BuildTestsFromMetadataAsy } var methodDataAttributeIndex = 0; - foreach (var methodDataSource in GetDataSources(metadata.DataSources)) + foreach (var methodDataSource in await GetDataSourcesAsync(metadata.DataSources)) { methodDataAttributeIndex++; var methodDataLoopIndex = 0; - await foreach (var methodDataFactory in methodDataSource.GetDataRowsAsync( + await foreach (var methodDataFactory in GetInitializedDataRowsAsync( + methodDataSource, DataGeneratorMetadataCreator.CreateDataGeneratorMetadata ( testMetadata: metadata, @@ -288,7 +301,7 @@ public async Task> BuildTestsFromMetadataAsy throw new InvalidOperationException($"Cannot create instance of generic type {metadata.TestClassType.Name} with empty type arguments"); } - var basicSkipReason = GetBasicSkipReason(metadata); + var basicSkipReason = GetBasicSkipReason(metadata, attributes); Func> instanceFactory; if (basicSkipReason is { Length: > 0 }) @@ -326,7 +339,8 @@ public async Task> BuildTestsFromMetadataAsy Events = new TestContextEvents(), ObjectBag = new Dictionary(), ClassConstructor = testBuilderContext.ClassConstructor, // Copy the ClassConstructor from the template - DataSourceAttribute = contextAccessor.Current.DataSourceAttribute // Copy any data source attribute + DataSourceAttribute = contextAccessor.Current.DataSourceAttribute, // Copy any data source attribute + InitializedAttributes = attributes // Pass the initialized attributes }; var test = await BuildTestAsync(metadata, testData, testSpecificContext); @@ -561,16 +575,45 @@ private static Type[] TryInferClassGenericsFromDataSources(TestMetadata metadata return resolvedTypes; } - private static IDataSourceAttribute[] GetDataSources(IDataSourceAttribute[] dataSources) + private async Task GetDataSourcesAsync(IDataSourceAttribute[] dataSources) { if (dataSources.Length == 0) { return [NoDataSource.Instance]; } + // Initialize all data sources to ensure properties are injected + foreach (var dataSource in dataSources) + { + await _dataSourceInitializer.EnsureInitializedAsync(dataSource); + } + return dataSources; } + /// + /// Ensures a data source is initialized before use and returns data rows. + /// This centralizes the initialization logic for all data source usage. + /// + private async IAsyncEnumerable>> GetInitializedDataRowsAsync( + IDataSourceAttribute dataSource, + DataGeneratorMetadata dataGeneratorMetadata) + { + // Ensure the data source is fully initialized before getting data rows + // This includes property injection and IAsyncInitializer.InitializeAsync + var initializedDataSource = await _dataSourceInitializer.EnsureInitializedAsync( + dataSource, + dataGeneratorMetadata.TestBuilderContext?.Current.ObjectBag, + dataGeneratorMetadata.TestInformation, + dataGeneratorMetadata.TestBuilderContext?.Current.Events); + + // Now get data rows from the initialized data source + await foreach (var dataRow in initializedDataSource.GetDataRowsAsync(dataGeneratorMetadata)) + { + yield return dataRow; + } + } + public async Task BuildTestAsync(TestMetadata metadata, TestData testData, TestBuilderContext testBuilderContext) { var testId = TestIdentifierService.GenerateTestId(metadata, testData); @@ -604,9 +647,9 @@ public async Task BuildTestAsync(TestMetadata metadata, /// Returns null if no skip attributes, a skip reason if basic skip attributes are found, /// or empty string if custom skip attributes requiring runtime evaluation are found. /// - private static string? GetBasicSkipReason(TestMetadata metadata) + private static string? GetBasicSkipReason(TestMetadata metadata, Attribute[]? cachedAttributes = null) { - var attributes = metadata.AttributeFactory(); + var attributes = cachedAttributes ?? metadata.AttributeFactory(); var skipAttributes = attributes.OfType().ToList(); if (skipAttributes.Count == 0) @@ -631,9 +674,10 @@ public async Task BuildTestAsync(TestMetadata metadata, } - private ValueTask CreateTestContextAsync(string testId, TestMetadata metadata, TestData testData, TestBuilderContext testBuilderContext) + private async ValueTask CreateTestContextAsync(string testId, TestMetadata metadata, TestData testData, TestBuilderContext testBuilderContext) { - var attributes = metadata.AttributeFactory.Invoke(); + // Use attributes from context if available, or create new ones + var attributes = testBuilderContext.InitializedAttributes ?? await InitializeAttributesAsync(metadata.AttributeFactory.Invoke()); var testDetails = new TestDetails { @@ -663,7 +707,7 @@ private ValueTask CreateTestContextAsync(string testId, TestMetadat context.TestDetails = testDetails; - return new ValueTask(context); + return context; } @@ -687,7 +731,7 @@ private async Task CreateFailedTestForDataGenerationErro { var testId = TestIdentifierService.GenerateFailedTestId(metadata, combination); - var testDetails = CreateFailedTestDetails(metadata, testId); + var testDetails = await CreateFailedTestDetails(metadata, testId); var context = CreateFailedTestContext(metadata, testDetails); await InvokeDiscoveryEventReceiversAsync(context); @@ -702,7 +746,7 @@ private async Task CreateFailedTestForDataGenerationErro }; } - private static TestDetails CreateFailedTestDetails(TestMetadata metadata, string testId) + private async Task CreateFailedTestDetails(TestMetadata metadata, string testId) { return new TestDetails { @@ -717,11 +761,27 @@ private static TestDetails CreateFailedTestDetails(TestMetadata metadata, string TestLineNumber = metadata.LineNumber, ReturnType = typeof(Task), MethodMetadata = metadata.MethodMetadata, - Attributes = metadata.AttributeFactory.Invoke(), + Attributes = await InitializeAttributesAsync(metadata.AttributeFactory.Invoke()), Timeout = TimeSpan.FromMinutes(30) // Default 30-minute timeout (can be overridden by TimeoutAttribute) }; } + private async Task InitializeAttributesAsync(Attribute[] attributes) + { + // Initialize any attributes that need property injection or implement IAsyncInitializer + // This ensures they're fully initialized before being used + foreach (var attribute in attributes) + { + if (attribute is IDataSourceAttribute dataSource) + { + // Data source attributes need to be initialized with property injection + await _dataSourceInitializer.EnsureInitializedAsync(dataSource); + } + } + + return attributes; + } + private TestContext CreateFailedTestContext(TestMetadata metadata, TestDetails testDetails) { var context = _contextProvider.CreateTestContext( @@ -1010,7 +1070,7 @@ public async IAsyncEnumerable BuildTestsStreamingAsync( } // Extract repeat count from attributes - var attributes = metadata.AttributeFactory.Invoke(); + var attributes = await InitializeAttributesAsync(metadata.AttributeFactory.Invoke()); var filteredAttributes = ScopedAttributeFilter.FilterScopedAttributes(attributes); var repeatAttr = filteredAttributes.OfType().FirstOrDefault(); var repeatCount = repeatAttr?.Times ?? 0; @@ -1020,7 +1080,8 @@ public async IAsyncEnumerable BuildTestsStreamingAsync( { TestMetadata = metadata.MethodMetadata, Events = new TestContextEvents(), - ObjectBag = new Dictionary() + ObjectBag = new Dictionary(), + InitializedAttributes = attributes // Store the initialized attributes }; // Check for ClassConstructor attribute and set it early if present @@ -1047,12 +1108,13 @@ public async IAsyncEnumerable BuildTestsStreamingAsync( // Stream through all data source combinations var classDataAttributeIndex = 0; - foreach (var classDataSource in GetDataSources(metadata.ClassDataSources)) + foreach (var classDataSource in await GetDataSourcesAsync(metadata.ClassDataSources)) { classDataAttributeIndex++; var classDataLoopIndex = 0; - await foreach (var classDataFactory in classDataSource.GetDataRowsAsync( + await foreach (var classDataFactory in GetInitializedDataRowsAsync( + classDataSource, DataGeneratorMetadataCreator.CreateDataGeneratorMetadata( testMetadata: metadata, testSessionId: _sessionId, @@ -1083,12 +1145,13 @@ public async IAsyncEnumerable BuildTestsStreamingAsync( // Stream through method data sources var methodDataAttributeIndex = 0; - foreach (var methodDataSource in GetDataSources(metadata.DataSources)) + foreach (var methodDataSource in await GetDataSourcesAsync(metadata.DataSources)) { methodDataAttributeIndex++; var methodDataLoopIndex = 0; - await foreach (var methodDataFactory in methodDataSource.GetDataRowsAsync( + await foreach (var methodDataFactory in GetInitializedDataRowsAsync( + methodDataSource, DataGeneratorMetadataCreator.CreateDataGeneratorMetadata( testMetadata: metadata, testSessionId: _sessionId, @@ -1240,7 +1303,8 @@ public async IAsyncEnumerable BuildTestsStreamingAsync( } // Create instance factory - var basicSkipReason = GetBasicSkipReason(metadata); + var attributes = contextAccessor.Current.InitializedAttributes ?? Array.Empty(); + var basicSkipReason = GetBasicSkipReason(metadata, attributes); Func> instanceFactory; if (basicSkipReason is { Length: > 0 }) @@ -1279,7 +1343,8 @@ public async IAsyncEnumerable BuildTestsStreamingAsync( Events = new TestContextEvents(), ObjectBag = new Dictionary(), ClassConstructor = contextAccessor.Current.ClassConstructor, // Preserve ClassConstructor if it was set - DataSourceAttribute = contextAccessor.Current.DataSourceAttribute // Preserve data source attribute + DataSourceAttribute = contextAccessor.Current.DataSourceAttribute, // Preserve data source attribute + InitializedAttributes = attributes // Pass the initialized attributes }; var test = await BuildTestAsync(metadata, testData, testSpecificContext); diff --git a/TUnit.Engine/Framework/TUnitServiceProvider.cs b/TUnit.Engine/Framework/TUnitServiceProvider.cs index b5dde1b703..8e5404ff8a 100644 --- a/TUnit.Engine/Framework/TUnitServiceProvider.cs +++ b/TUnit.Engine/Framework/TUnitServiceProvider.cs @@ -7,6 +7,8 @@ using Microsoft.Testing.Platform.Requests; using Microsoft.Testing.Platform.Services; using TUnit.Core; +using TUnit.Core.DataSources; +using TUnit.Core.Initialization; using TUnit.Core.Interfaces; using TUnit.Engine.Building; using TUnit.Engine.Building.Collectors; @@ -47,6 +49,9 @@ public ITestExecutionFilter? Filter public TUnitInitializer Initializer { get; } public CancellationTokenSource FailFastCancellationSource { get; } public ParallelLimitLockProvider ParallelLimitLockProvider { get; } + public PropertyInjectionService PropertyInjectionService { get; } + public DataSourceInitializer DataSourceInitializer { get; } + public TestObjectInitializer TestObjectInitializer { get; } public TUnitServiceProvider(IExtension extension, ExecuteRequestContext context, @@ -76,8 +81,17 @@ public TUnitServiceProvider(IExtension extension, loggerFactory.CreateLogger(), VerbosityService)); + // Create initialization services early as they're needed by other services + DataSourceInitializer = Register(new DataSourceInitializer()); + PropertyInjectionService = Register(new PropertyInjectionService(DataSourceInitializer)); + TestObjectInitializer = Register(new TestObjectInitializer(PropertyInjectionService)); + + // Initialize the circular dependencies + PropertyInjectionService.Initialize(TestObjectInitializer); + DataSourceInitializer.Initialize(PropertyInjectionService); + // Register the test argument tracking service to handle object disposal for shared instances - var testArgumentTrackingService = Register(new TestArgumentTrackingService()); + var testArgumentTrackingService = Register(new TestArgumentTrackingService(TestObjectInitializer)); TestFilterService = Register(new TestFilterService(Logger, testArgumentTrackingService)); @@ -126,7 +140,7 @@ public TUnitServiceProvider(IExtension extension, #pragma warning restore IL2026 var testBuilder = Register( - new TestBuilder(TestSessionId, EventReceiverOrchestrator, ContextProvider)); + new TestBuilder(TestSessionId, EventReceiverOrchestrator, ContextProvider, PropertyInjectionService, DataSourceInitializer)); TestBuilderPipeline = Register( new TestBuilderPipeline( @@ -140,7 +154,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); + var testInitializer = new TestInitializer(EventReceiverOrchestrator, TestObjectInitializer); // Create the new TestCoordinator that orchestrates the granular services var testCoordinator = Register( diff --git a/TUnit.Engine/Services/TestArgumentTrackingService.cs b/TUnit.Engine/Services/TestArgumentTrackingService.cs index 5bee4a9f56..ecf2064326 100644 --- a/TUnit.Engine/Services/TestArgumentTrackingService.cs +++ b/TUnit.Engine/Services/TestArgumentTrackingService.cs @@ -11,6 +11,13 @@ namespace TUnit.Engine.Services; /// internal sealed class TestArgumentTrackingService : ITestRegisteredEventReceiver { + private readonly TestObjectInitializer _testObjectInitializer; + + public TestArgumentTrackingService(TestObjectInitializer testObjectInitializer) + { + _testObjectInitializer = testObjectInitializer; + } + public int Order => int.MinValue; // Run first to ensure tracking happens before other event receivers /// @@ -25,14 +32,14 @@ public async ValueTask OnTestRegistered(TestRegisteredContext context) // Use centralized TestObjectInitializer for all initialization // Initialize class arguments - await TestObjectInitializer.InitializeArgumentsAsync( + await _testObjectInitializer.InitializeArgumentsAsync( classArguments, testContext.ObjectBag, testContext.TestDetails.MethodMetadata, testContext.Events); // Initialize method arguments - await TestObjectInitializer.InitializeArgumentsAsync( + await _testObjectInitializer.InitializeArgumentsAsync( methodArguments, testContext.ObjectBag, testContext.TestDetails.MethodMetadata, diff --git a/TUnit.Engine/TestInitializer.cs b/TUnit.Engine/TestInitializer.cs index c2db84f934..f9146054cc 100644 --- a/TUnit.Engine/TestInitializer.cs +++ b/TUnit.Engine/TestInitializer.cs @@ -8,16 +8,18 @@ namespace TUnit.Engine; internal class TestInitializer { private readonly EventReceiverOrchestrator _eventReceiverOrchestrator; + private readonly TestObjectInitializer _testObjectInitializer; - public TestInitializer(EventReceiverOrchestrator eventReceiverOrchestrator) + public TestInitializer(EventReceiverOrchestrator eventReceiverOrchestrator, TestObjectInitializer testObjectInitializer) { _eventReceiverOrchestrator = eventReceiverOrchestrator; + _testObjectInitializer = testObjectInitializer; } public async Task InitializeTest(AbstractExecutableTest test, CancellationToken cancellationToken) { // Use centralized TestObjectInitializer for all initialization - await TestObjectInitializer.InitializeTestClassAsync( + await _testObjectInitializer.InitializeTestClassAsync( test.Context.TestDetails.ClassInstance, test.Context); 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 53cd2074da..f0bdd468d3 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 @@ -125,7 +125,7 @@ namespace protected AsyncDataSourceGeneratorAttribute() { } protected abstract .<<.>> GenerateDataSourcesAsync(.DataGeneratorMetadata dataGeneratorMetadata); [.(typeof(.AsyncDataSourceGeneratorAttribute.d__1))] - public override .<<.>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } + public override sealed .<<.>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } } [(.Class | .Method, AllowMultiple=true)] public abstract class AsyncDataSourceGeneratorAttribute<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T1, [.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T2> : .TypedDataSourceAttribute<> @@ -133,7 +133,7 @@ namespace protected AsyncDataSourceGeneratorAttribute() { } protected abstract .<<.<>>> GenerateDataSourcesAsync(.DataGeneratorMetadata dataGeneratorMetadata); [.(typeof(.AsyncDataSourceGeneratorAttribute.d__1))] - public override .<<.<>>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } + public override sealed .<<.<>>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } } [(.Class | .Method, AllowMultiple=true)] public abstract class AsyncDataSourceGeneratorAttribute<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T1, [.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T2, [.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T3> : .TypedDataSourceAttribute<> @@ -141,7 +141,7 @@ namespace protected AsyncDataSourceGeneratorAttribute() { } protected abstract .<<.<>>> GenerateDataSourcesAsync(.DataGeneratorMetadata dataGeneratorMetadata); [.(typeof(.AsyncDataSourceGeneratorAttribute.d__1))] - public override .<<.<>>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } + public override sealed .<<.<>>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } } [(.Class | .Method, AllowMultiple=true)] public abstract class AsyncDataSourceGeneratorAttribute<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T1, [.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T2, [.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T3, [.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T4> : .TypedDataSourceAttribute<> @@ -1046,18 +1046,6 @@ namespace public string PropertyName { get; } public PropertyType { get; } } - public sealed class PropertyInjectionService - { - public PropertyInjectionService() { } - public static .PropertyInjectionData CreatePropertyInjection(.PropertyInfo property) { } - public static CreatePropertySetter(.PropertyInfo property) { } - [.("Trimming", "IL2070", Justification="Legacy compatibility method")] - public static .PropertyInjectionData[] DiscoverInjectableProperties([.(..None | ..PublicFields | ..NonPublicFields | ..PublicProperties)] type) { } - [.("Trimming", "IL2072", Justification="Legacy compatibility method")] - public static . InjectPropertiesAsync(.TestContext testContext, object instance, .PropertyDataSource[] propertyDataSources, .PropertyInjectionData[] injectionData, .MethodMetadata testInformation, string testSessionId) { } - public static . InjectPropertiesIntoArgumentsAsync(object?[] arguments, . objectBag, .MethodMetadata methodMetadata, .TestContextEvents events) { } - public static . InjectPropertiesIntoObjectAsync(object instance, .? objectBag, .MethodMetadata? methodMetadata, .TestContextEvents? events) { } - } [.DebuggerDisplay("{Type} {Name})")] public class PropertyMetadata : .MemberMetadata, <.PropertyMetadata> { @@ -2135,6 +2123,14 @@ namespace .Hooks public abstract . ExecuteAsync(T context, .CancellationToken cancellationToken); } } +namespace .Initialization +{ + public class TestObjectInitializationException : + { + public TestObjectInitializationException(string message) { } + public TestObjectInitializationException(string message, innerException) { } + } +} namespace .Interfaces { public sealed class CompileTimeResolvedData 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 4836df438b..5310578f52 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 @@ -125,7 +125,7 @@ namespace protected AsyncDataSourceGeneratorAttribute() { } protected abstract .<<.>> GenerateDataSourcesAsync(.DataGeneratorMetadata dataGeneratorMetadata); [.(typeof(.AsyncDataSourceGeneratorAttribute.d__1))] - public override .<<.>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } + public override sealed .<<.>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } } [(.Class | .Method, AllowMultiple=true)] public abstract class AsyncDataSourceGeneratorAttribute<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T1, [.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T2> : .TypedDataSourceAttribute<> @@ -133,7 +133,7 @@ namespace protected AsyncDataSourceGeneratorAttribute() { } protected abstract .<<.<>>> GenerateDataSourcesAsync(.DataGeneratorMetadata dataGeneratorMetadata); [.(typeof(.AsyncDataSourceGeneratorAttribute.d__1))] - public override .<<.<>>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } + public override sealed .<<.<>>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } } [(.Class | .Method, AllowMultiple=true)] public abstract class AsyncDataSourceGeneratorAttribute<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T1, [.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T2, [.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T3> : .TypedDataSourceAttribute<> @@ -141,7 +141,7 @@ namespace protected AsyncDataSourceGeneratorAttribute() { } protected abstract .<<.<>>> GenerateDataSourcesAsync(.DataGeneratorMetadata dataGeneratorMetadata); [.(typeof(.AsyncDataSourceGeneratorAttribute.d__1))] - public override .<<.<>>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } + public override sealed .<<.<>>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } } [(.Class | .Method, AllowMultiple=true)] public abstract class AsyncDataSourceGeneratorAttribute<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T1, [.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T2, [.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T3, [.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] T4> : .TypedDataSourceAttribute<> @@ -1046,18 +1046,6 @@ namespace public string PropertyName { get; } public PropertyType { get; } } - public sealed class PropertyInjectionService - { - public PropertyInjectionService() { } - public static .PropertyInjectionData CreatePropertyInjection(.PropertyInfo property) { } - public static CreatePropertySetter(.PropertyInfo property) { } - [.("Trimming", "IL2070", Justification="Legacy compatibility method")] - public static .PropertyInjectionData[] DiscoverInjectableProperties([.(..None | ..PublicFields | ..NonPublicFields | ..PublicProperties)] type) { } - [.("Trimming", "IL2072", Justification="Legacy compatibility method")] - public static . InjectPropertiesAsync(.TestContext testContext, object instance, .PropertyDataSource[] propertyDataSources, .PropertyInjectionData[] injectionData, .MethodMetadata testInformation, string testSessionId) { } - public static . InjectPropertiesIntoArgumentsAsync(object?[] arguments, . objectBag, .MethodMetadata methodMetadata, .TestContextEvents events) { } - public static . InjectPropertiesIntoObjectAsync(object instance, .? objectBag, .MethodMetadata? methodMetadata, .TestContextEvents? events) { } - } [.DebuggerDisplay("{Type} {Name})")] public class PropertyMetadata : .MemberMetadata, <.PropertyMetadata> { @@ -2135,6 +2123,14 @@ namespace .Hooks public abstract . ExecuteAsync(T context, .CancellationToken cancellationToken); } } +namespace .Initialization +{ + public class TestObjectInitializationException : + { + public TestObjectInitializationException(string message) { } + public TestObjectInitializationException(string message, innerException) { } + } +} namespace .Interfaces { public sealed class CompileTimeResolvedData 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 0c76528400..d19316549d 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 @@ -121,7 +121,7 @@ namespace protected AsyncDataSourceGeneratorAttribute() { } protected abstract .<<.>> GenerateDataSourcesAsync(.DataGeneratorMetadata dataGeneratorMetadata); [.(typeof(.AsyncDataSourceGeneratorAttribute.d__1))] - public override .<<.>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } + public override sealed .<<.>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } } [(.Class | .Method, AllowMultiple=true)] public abstract class AsyncDataSourceGeneratorAttribute : .TypedDataSourceAttribute<> @@ -129,7 +129,7 @@ namespace protected AsyncDataSourceGeneratorAttribute() { } protected abstract .<<.<>>> GenerateDataSourcesAsync(.DataGeneratorMetadata dataGeneratorMetadata); [.(typeof(.AsyncDataSourceGeneratorAttribute.d__1))] - public override .<<.<>>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } + public override sealed .<<.<>>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } } [(.Class | .Method, AllowMultiple=true)] public abstract class AsyncDataSourceGeneratorAttribute : .TypedDataSourceAttribute<> @@ -137,7 +137,7 @@ namespace protected AsyncDataSourceGeneratorAttribute() { } protected abstract .<<.<>>> GenerateDataSourcesAsync(.DataGeneratorMetadata dataGeneratorMetadata); [.(typeof(.AsyncDataSourceGeneratorAttribute.d__1))] - public override .<<.<>>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } + public override sealed .<<.<>>> GetTypedDataRowsAsync(.DataGeneratorMetadata dataGeneratorMetadata) { } } [(.Class | .Method, AllowMultiple=true)] public abstract class AsyncDataSourceGeneratorAttribute : .TypedDataSourceAttribute<> @@ -983,16 +983,6 @@ namespace public string PropertyName { get; } public PropertyType { get; } } - public sealed class PropertyInjectionService - { - public PropertyInjectionService() { } - public static .PropertyInjectionData CreatePropertyInjection(.PropertyInfo property) { } - public static CreatePropertySetter(.PropertyInfo property) { } - public static .PropertyInjectionData[] DiscoverInjectableProperties( type) { } - public static . InjectPropertiesAsync(.TestContext testContext, object instance, .PropertyDataSource[] propertyDataSources, .PropertyInjectionData[] injectionData, .MethodMetadata testInformation, string testSessionId) { } - public static . InjectPropertiesIntoArgumentsAsync(object?[] arguments, . objectBag, .MethodMetadata methodMetadata, .TestContextEvents events) { } - public static . InjectPropertiesIntoObjectAsync(object instance, .? objectBag, .MethodMetadata? methodMetadata, .TestContextEvents? events) { } - } [.DebuggerDisplay("{Type} {Name})")] public class PropertyMetadata : .MemberMetadata, <.PropertyMetadata> { @@ -2023,6 +2013,14 @@ namespace .Hooks public abstract . ExecuteAsync(T context, .CancellationToken cancellationToken); } } +namespace .Initialization +{ + public class TestObjectInitializationException : + { + public TestObjectInitializationException(string message) { } + public TestObjectInitializationException(string message, innerException) { } + } +} namespace .Interfaces { public sealed class CompileTimeResolvedData