Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion TUnit.Core/DataGeneratorMetadataCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ public static DataGeneratorMetadata CreateForPropertyInjection(
PropertyMetadata propertyMetadata,
MethodMetadata? methodMetadata,
IDataSourceAttribute dataSource,
string testSessionId,
TestContext? testContext = null,
object? testClassInstance = null,
TestContextEvents? events = null,
Expand All @@ -193,7 +194,7 @@ public static DataGeneratorMetadata CreateForPropertyInjection(
MembersToGenerate = [propertyMetadata],
TestInformation = methodMetadata,
Type = DataGeneratorType.Property,
TestSessionId = TestSessionContext.Current?.Id ?? "property-injection",
TestSessionId = testSessionId,
TestClassInstance = testClassInstance ?? testContext?.Metadata.TestDetails.ClassInstance,
ClassInstanceArguments = testContext?.Metadata.TestDetails.TestClassArguments ?? []
};
Expand All @@ -215,6 +216,7 @@ public static DataGeneratorMetadata CreateForPropertyInjection(
Type containingType,
MethodMetadata? methodMetadata,
IDataSourceAttribute dataSource,
string testSessionId,
TestContext? testContext = null,
object? testClassInstance = null,
TestContextEvents? events = null,
Expand All @@ -235,6 +237,7 @@ public static DataGeneratorMetadata CreateForPropertyInjection(
propertyMetadata,
methodMetadata,
dataSource,
testSessionId,
testContext,
testClassInstance,
events,
Expand Down
1 change: 1 addition & 0 deletions TUnit.Core/Helpers/DataSourceHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ public static void RegisterTypeCreator<T>(Func<MethodMetadata, string, Task<T>>
containingType,
testInformation,
dataSourceAttribute,
testSessionId,
TestContext.Current,
TestContext.Current?.Metadata.TestDetails.ClassInstance,
TestContext.Current?.InternalEvents,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public sealed class PropertyInjectionMetadata
/// <summary>
/// Gets the type that contains the property (the parent class).
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)]
public required Type ContainingType { get; init; }

/// <summary>
Expand Down
9 changes: 7 additions & 2 deletions TUnit.Core/PropertyInjection/PropertyHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ internal static class PropertyHelper
{
/// <summary>
/// Gets PropertyInfo in an AOT-safe manner.
/// Searches for both public and non-public properties to support internal properties.
/// </summary>
public static PropertyInfo GetPropertyInfo(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)]
Type containingType,
string propertyName)
{
var property = containingType.GetProperty(propertyName);
// Use binding flags to find both public and non-public properties
// This is necessary to support internal properties on internal classes
var property = containingType.GetProperty(
propertyName,
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);

if (property == null)
{
Expand Down
50 changes: 5 additions & 45 deletions TUnit.Core/PropertyInjection/PropertyInjectionCache.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
using System.Diagnostics.CodeAnalysis;
using TUnit.Core.Data;
using TUnit.Core.Data;

namespace TUnit.Core.PropertyInjection;

/// <summary>
/// Provides caching functionality for property injection operations.
/// Follows Single Responsibility Principle by focusing only on caching.
/// Provides pure caching functionality for property injection metadata.
/// Follows Single Responsibility Principle - only caches type metadata, no execution logic.
///
/// This cache supports both execution modes:
/// - Source Generation Mode: Uses pre-compiled property setters and metadata
/// - Reflection Mode: Uses runtime discovery and dynamic property access
///
/// The IL2067 suppressions are necessary because types come from runtime objects
/// (via GetType() calls) which cannot have compile-time annotations.
/// Instance-level injection tracking has been moved to ObjectLifecycleService
/// to maintain SRP (caching vs execution are separate concerns).
/// </summary>
internal static class PropertyInjectionCache
{
private static readonly ThreadSafeDictionary<Type, PropertyInjectionPlan> _injectionPlans = new();
private static readonly ThreadSafeDictionary<Type, bool> _shouldInjectCache = new();
private static readonly ThreadSafeDictionary<object, TaskCompletionSource<bool>> _injectionTasks = new();

/// <summary>
/// Gets or creates an injection plan for the specified type.
Expand All @@ -41,42 +39,4 @@ public static bool HasInjectableProperties(Type type)
return plan.HasProperties;
});
}

/// <summary>
/// Ensures properties are injected into the specified instance.
/// Fast-path optimized for already-injected instances (zero allocation).
/// </summary>
public static async ValueTask EnsureInjectedAsync(object instance, Func<object, ValueTask> injectionFactory)
{
if (_injectionTasks.TryGetValue(instance, out var existingTcs) && existingTcs.Task.IsCompleted)
{
if (existingTcs.Task.IsFaulted)
{
await existingTcs.Task.ConfigureAwait(false);
}

return;
}

var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
existingTcs = _injectionTasks.GetOrAdd(instance, _ => tcs);

if (existingTcs == tcs)
{
try
{
await injectionFactory(instance).ConfigureAwait(false);
tcs.SetResult(true);
}
catch (Exception ex)
{
tcs.SetException(ex);
throw;
}
}
else
{
await existingTcs.Task.ConfigureAwait(false);
}
}
}
21 changes: 9 additions & 12 deletions TUnit.Engine/Building/TestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ internal sealed class TestBuilder : ITestBuilder
private readonly string _sessionId;
private readonly EventReceiverOrchestrator _eventReceiverOrchestrator;
private readonly IContextProvider _contextProvider;
private readonly PropertyInjectionService _propertyInjectionService;
private readonly DataSourceInitializer _dataSourceInitializer;
private readonly ObjectLifecycleService _objectLifecycleService;
private readonly Discovery.IHookDiscoveryService _hookDiscoveryService;
private readonly TestArgumentRegistrationService _testArgumentRegistrationService;
private readonly IMetadataFilterMatcher _filterMatcher;
Expand All @@ -31,8 +30,7 @@ public TestBuilder(
string sessionId,
EventReceiverOrchestrator eventReceiverOrchestrator,
IContextProvider contextProvider,
PropertyInjectionService propertyInjectionService,
DataSourceInitializer dataSourceInitializer,
ObjectLifecycleService objectLifecycleService,
Discovery.IHookDiscoveryService hookDiscoveryService,
TestArgumentRegistrationService testArgumentRegistrationService,
IMetadataFilterMatcher filterMatcher)
Expand All @@ -41,8 +39,7 @@ public TestBuilder(
_hookDiscoveryService = hookDiscoveryService;
_eventReceiverOrchestrator = eventReceiverOrchestrator;
_contextProvider = contextProvider;
_propertyInjectionService = propertyInjectionService;
_dataSourceInitializer = dataSourceInitializer;
_objectLifecycleService = objectLifecycleService;
_testArgumentRegistrationService = testArgumentRegistrationService;
_filterMatcher = filterMatcher ?? throw new ArgumentNullException(nameof(filterMatcher));
}
Expand Down Expand Up @@ -262,7 +259,7 @@ public async Task<IEnumerable<AbstractExecutableTest>> BuildTestsFromMetadataAsy
var tempObjectBag = new ConcurrentDictionary<string, object?>();
var tempEvents = new TestContextEvents();

await _propertyInjectionService.InjectPropertiesIntoObjectAsync(
await _objectLifecycleService.RegisterObjectAsync(
instanceForMethodDataSources,
tempObjectBag,
metadata.MethodMetadata,
Expand Down Expand Up @@ -772,7 +769,7 @@ private async Task<IDataSourceAttribute[]> GetDataSourcesAsync(IDataSourceAttrib
// Initialize all data sources to ensure properties are injected
foreach (var dataSource in dataSources)
{
await _dataSourceInitializer.EnsureInitializedAsync(dataSource);
await _objectLifecycleService.EnsureInitializedAsync(dataSource);
}

return dataSources;
Expand All @@ -788,7 +785,7 @@ private async Task<IDataSourceAttribute[]> GetDataSourcesAsync(IDataSourceAttrib
{
// Ensure the data source is fully initialized before getting data rows
// This includes property injection and IAsyncInitializer.InitializeAsync
var initializedDataSource = await _dataSourceInitializer.EnsureInitializedAsync(
var initializedDataSource = await _objectLifecycleService.EnsureInitializedAsync(
dataSource,
dataGeneratorMetadata.TestBuilderContext.Current.StateBag,
dataGeneratorMetadata.TestInformation,
Expand Down Expand Up @@ -952,7 +949,7 @@ private async Task InvokeTestRegisteredEventReceiversAsync(TestContext context)
context.InternalDiscoveredTest = discoveredTest;

// First, invoke the global test argument registration service to register shared instances
await _testArgumentRegistrationService.OnTestRegistered(registeredContext);
await _testArgumentRegistrationService.RegisterTestArgumentsAsync(context);

var eventObjects = context.GetEligibleEventObjects();

Expand Down Expand Up @@ -1027,7 +1024,7 @@ private async Task<Attribute[]> InitializeAttributesAsync(Attribute[] attributes
if (attribute is IDataSourceAttribute dataSource)
{
// Data source attributes need to be initialized with property injection
await _dataSourceInitializer.EnsureInitializedAsync(dataSource);
await _objectLifecycleService.EnsureInitializedAsync(dataSource);
}
}

Expand Down Expand Up @@ -1395,7 +1392,7 @@ public async IAsyncEnumerable<AbstractExecutableTest> BuildTestsStreamingAsync(
var tempObjectBag = new ConcurrentDictionary<string, object?>();
var tempEvents = new TestContextEvents();

await _propertyInjectionService.InjectPropertiesIntoObjectAsync(
await _objectLifecycleService.RegisterObjectAsync(
instanceForMethodDataSources,
tempObjectBag,
metadata.MethodMetadata,
Expand Down
34 changes: 17 additions & 17 deletions TUnit.Engine/Framework/TUnitServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ public ITestExecutionFilter? Filter
public TUnitInitializer Initializer { get; }
public CancellationTokenSource FailFastCancellationSource { get; }
public ParallelLimitLockProvider ParallelLimitLockProvider { get; }
public PropertyInjectionService PropertyInjectionService { get; }
public DataSourceInitializer DataSourceInitializer { get; }
public ObjectRegistrationService ObjectRegistrationService { get; }
public ObjectLifecycleService ObjectLifecycleService { get; }
public bool AfterSessionHooksFailed { get; set; }

[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Reflection mode is not used in AOT/trimmed scenarios")]
Expand Down Expand Up @@ -104,25 +102,27 @@ public TUnitServiceProvider(IExtension extension,
loggerFactory.CreateLogger<TUnitFrameworkLogger>(),
logLevelProvider));

// Create initialization services
// Note: Circular dependency managed through two-phase initialization
// Phase 1: Create services with partial dependencies
DataSourceInitializer = Register(new DataSourceInitializer());
PropertyInjectionService = Register(new PropertyInjectionService(DataSourceInitializer));
ObjectRegistrationService = Register(new ObjectRegistrationService(PropertyInjectionService));

// Phase 2: Complete dependencies (Initialize methods accept IObjectRegistry to break circular dependency)
PropertyInjectionService.Initialize(ObjectRegistrationService);
DataSourceInitializer.Initialize(PropertyInjectionService);
// Create initialization services using Lazy<T> to break circular dependencies
// No more two-phase initialization with Initialize() calls
var objectGraphDiscoveryService = Register(new ObjectGraphDiscoveryService());

// Keep TrackableObjectGraphProvider for ObjectTracker (TUnit.Core dependency)
var trackableObjectGraphProvider = new TrackableObjectGraphProvider();

var disposer = new Disposer(Logger);

var objectTracker = new ObjectTracker(trackableObjectGraphProvider, disposer);

// Use Lazy<T> to break circular dependency between PropertyInjector and ObjectLifecycleService
ObjectLifecycleService? objectLifecycleServiceInstance = null;
var lazyObjectLifecycleService = new Lazy<ObjectLifecycleService>(() => objectLifecycleServiceInstance!);
var lazyPropertyInjector = new Lazy<PropertyInjector>(() => new PropertyInjector(lazyObjectLifecycleService, TestSessionId));

objectLifecycleServiceInstance = new ObjectLifecycleService(lazyPropertyInjector, objectGraphDiscoveryService, objectTracker);
ObjectLifecycleService = Register(objectLifecycleServiceInstance);

// Register the test argument registration service to handle object registration for shared instances
var testArgumentRegistrationService = Register(new TestArgumentRegistrationService(ObjectRegistrationService, objectTracker));
var testArgumentRegistrationService = Register(new TestArgumentRegistrationService(ObjectLifecycleService));

TestFilterService = Register(new TestFilterService(Logger, testArgumentRegistrationService));

Expand All @@ -135,7 +135,7 @@ public TUnitServiceProvider(IExtension extension,

CancellationToken = Register(new EngineCancellationToken());

EventReceiverOrchestrator = Register(new EventReceiverOrchestrator(Logger, trackableObjectGraphProvider));
EventReceiverOrchestrator = Register(new EventReceiverOrchestrator(Logger));
HookCollectionService = Register<IHookCollectionService>(new HookCollectionService(EventReceiverOrchestrator));

ParallelLimitLockProvider = Register(new ParallelLimitLockProvider());
Expand Down Expand Up @@ -174,7 +174,7 @@ public TUnitServiceProvider(IExtension extension,
var dependencyExpander = Register(new MetadataDependencyExpander(filterMatcher));

var testBuilder = Register<ITestBuilder>(
new TestBuilder(TestSessionId, EventReceiverOrchestrator, ContextProvider, PropertyInjectionService, DataSourceInitializer, hookDiscoveryService, testArgumentRegistrationService, filterMatcher));
new TestBuilder(TestSessionId, EventReceiverOrchestrator, ContextProvider, ObjectLifecycleService, hookDiscoveryService, testArgumentRegistrationService, filterMatcher));

TestBuilderPipeline = Register(
new TestBuilderPipeline(
Expand All @@ -188,7 +188,7 @@ public TUnitServiceProvider(IExtension extension,
// Create test finder service after discovery service so it can use its cache
TestFinder = Register<ITestFinder>(new TestFinder(DiscoveryService));

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

// Create the new TestCoordinator that orchestrates the granular services
var testCoordinator = Register<ITestCoordinator>(
Expand Down
Loading
Loading