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
Original file line number Diff line number Diff line change
Expand Up @@ -65,33 +65,57 @@

var processedProperties = new HashSet<string>();

var properties = typeSymbol.GetMembersIncludingBase()
var directProperties = typeSymbol.GetMembers()
.OfType<IPropertySymbol>()
.Where(CanSetProperty);
.Where(CanSetProperty)
.ToList();

foreach (var property in properties)
var inheritedProperties = typeSymbol.GetMembersIncludingBase()
.OfType<IPropertySymbol>()
.Where(CanSetProperty)
.Where(p => p.ContainingType != typeSymbol)

Check warning on line 76 in TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

Verwenden von "SymbolEqualityComparer" beim Vergleichen von Symbolen

Check warning on line 76 in TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (fr-FR)

Utiliser ’SymbolEqualityComparer’ lors de la comparaison de symboles

Check warning on line 76 in TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

Użyj elementu „SymbolEqualityComparer” podczas porównywania symboli

Check warning on line 76 in TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Use 'SymbolEqualityComparer' when comparing symbols

Check warning on line 76 in TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Use 'SymbolEqualityComparer' when comparing symbols

Check warning on line 76 in TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Use 'SymbolEqualityComparer' when comparing symbols

Check warning on line 76 in TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Use 'SymbolEqualityComparer' when comparing symbols

Check warning on line 76 in TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Use 'SymbolEqualityComparer' when comparing symbols

Check warning on line 76 in TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Use 'SymbolEqualityComparer' when comparing symbols

Check warning on line 76 in TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Use 'SymbolEqualityComparer' when comparing symbols

Check warning on line 76 in TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Use 'SymbolEqualityComparer' when comparing symbols

Check warning on line 76 in TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Use 'SymbolEqualityComparer' when comparing symbols

Check warning on line 76 in TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Use 'SymbolEqualityComparer' when comparing symbols

Check warning on line 76 in TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Use 'SymbolEqualityComparer' when comparing symbols

Check warning on line 76 in TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Use 'SymbolEqualityComparer' when comparing symbols
.ToList();

foreach (var property in directProperties)
{
if (!processedProperties.Add(property.Name))
if (processedProperties.Add(property.Name))
{
continue;
foreach (var attr in property.GetAttributes())
{
if (attr.AttributeClass != null &&
attr.AttributeClass.AllInterfaces.Contains(dataSourceInterface, SymbolEqualityComparer.Default))
{
propertiesWithDataSources.Add(new PropertyWithDataSourceAttribute
{
Property = property,
DataSourceAttribute = attr
});
break;
}
}
}
}

foreach (var attr in property.GetAttributes())
foreach (var property in inheritedProperties)
{
if (processedProperties.Add(property.Name))
{
if (attr.AttributeClass != null &&
attr.AttributeClass.AllInterfaces.Contains(dataSourceInterface, SymbolEqualityComparer.Default))
foreach (var attr in property.GetAttributes())
{
propertiesWithDataSources.Add(new PropertyWithDataSourceAttribute
if (attr.AttributeClass != null &&
attr.AttributeClass.AllInterfaces.Contains(dataSourceInterface, SymbolEqualityComparer.Default))
{
Property = property,
DataSourceAttribute = attr
});
break; // Only one data source per property
propertiesWithDataSources.Add(new PropertyWithDataSourceAttribute
{
Property = property,
DataSourceAttribute = attr
});
break;
}
}
}
}

// Only return if there are actually properties with data sources
if (propertiesWithDataSources.Count == 0)
{
return null;
Expand Down
8 changes: 4 additions & 4 deletions TUnit.Engine/Framework/TUnitServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,14 @@ public TUnitServiceProvider(IExtension extension,
loggerFactory.CreateLogger<TUnitFrameworkLogger>(),
logLevelProvider));

// Create initialization services early as they're needed by other services
// 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));

// NEW: Separate registration and execution services (replaces TestObjectInitializer)
ObjectRegistrationService = Register(new ObjectRegistrationService(PropertyInjectionService));

// Initialize the circular dependencies
// Phase 2: Complete dependencies (Initialize methods accept IObjectRegistry to break circular dependency)
PropertyInjectionService.Initialize(ObjectRegistrationService);
DataSourceInitializer.Initialize(PropertyInjectionService);

Expand Down
8 changes: 6 additions & 2 deletions TUnit.Engine/Services/DataSourceInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ internal sealed class DataSourceInitializer
private readonly object _lock = new();
private PropertyInjectionService? _propertyInjectionService;

/// <summary>
/// Completes initialization by providing the PropertyInjectionService.
/// This two-phase initialization breaks the circular dependency.
/// </summary>
public void Initialize(PropertyInjectionService propertyInjectionService)
{
_propertyInjectionService = propertyInjectionService;
_propertyInjectionService = propertyInjectionService ?? throw new ArgumentNullException(nameof(propertyInjectionService));
}

/// <summary>
Expand Down Expand Up @@ -72,7 +76,7 @@ private async Task InitializeDataSourceAsync(
events ??= new TestContextEvents();

// Initialize the data source directly here
// Step 1: Property injection - use PropertyInjectionService if available
// Step 1: Property injection (if PropertyInjectionService has been initialized)
if (_propertyInjectionService != null && PropertyInjectionCache.HasInjectableProperties(dataSource.GetType()))
{
await _propertyInjectionService.InjectPropertiesIntoObjectAsync(
Expand Down
35 changes: 35 additions & 0 deletions TUnit.Engine/Services/IObjectRegistry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Collections.Concurrent;
using System.Threading.Tasks;
using TUnit.Core;

namespace TUnit.Engine.Services;

/// <summary>
/// Interface for registering objects during test discovery and execution.
/// Used to break circular dependencies between property injection and object registration services.
/// </summary>
internal interface IObjectRegistry
{
/// <summary>
/// Registers a single object during the registration phase.
/// Injects properties, tracks for disposal (once), but does NOT call IAsyncInitializer.
/// </summary>
/// <param name="instance">The object instance to register. Must not be null.</param>
/// <param name="objectBag">Shared object bag for the test context. Must not be null.</param>
/// <param name="methodMetadata">Method metadata for the test. Can be null.</param>
/// <param name="events">Test context events for tracking. Must not be null and must be unique per test permutation.</param>
Task RegisterObjectAsync(
object instance,
ConcurrentDictionary<string, object?> objectBag,
MethodMetadata? methodMetadata,
TestContextEvents events);

/// <summary>
/// Registers multiple argument objects during the registration phase.
/// </summary>
Task RegisterArgumentsAsync(
object?[] arguments,
ConcurrentDictionary<string, object?> objectBag,
MethodMetadata? methodMetadata,
TestContextEvents events);
}
4 changes: 2 additions & 2 deletions TUnit.Engine/Services/ObjectRegistrationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace TUnit.Engine.Services;
/// Responsibilities: Create instances, inject properties, track for disposal (ONCE per object).
/// Does NOT call IAsyncInitializer - that's deferred to ObjectInitializationService during execution.
/// </summary>
internal sealed class ObjectRegistrationService
internal sealed class ObjectRegistrationService : IObjectRegistry
{
private readonly PropertyInjectionService _propertyInjectionService;

Expand Down Expand Up @@ -67,7 +67,7 @@ await _propertyInjectionService.InjectPropertiesIntoObjectAsync(
public async Task RegisterArgumentsAsync(
object?[] arguments,
ConcurrentDictionary<string, object?> objectBag,
MethodMetadata methodMetadata,
MethodMetadata? methodMetadata,
TestContextEvents events)
{
if (arguments == null || arguments.Length == 0)
Expand Down
12 changes: 5 additions & 7 deletions TUnit.Engine/Services/PropertyDataResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal static class PropertyDataResolver
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Property data resolution uses reflection on property types")]
#endif
public static async Task<object?> ResolvePropertyDataAsync(PropertyInitializationContext context, DataSourceInitializer dataSourceInitializer, ObjectRegistrationService objectRegistrationService)
public static async Task<object?> ResolvePropertyDataAsync(PropertyInitializationContext context, DataSourceInitializer dataSourceInitializer, IObjectRegistry objectRegistry)
{
var dataSource = await GetInitializedDataSourceAsync(context, dataSourceInitializer);
if (dataSource == null)
Expand All @@ -43,7 +43,8 @@ internal static class PropertyDataResolver
// Initialize the resolved value if needed
if (value != null)
{
// If the resolved value is itself a data source, ensure it's initialized
// Ensure the value is fully initialized (property injection + IAsyncInitializer)
// DataSourceInitializer handles both data sources and regular objects
if (value is IDataSourceAttribute dataSourceValue)
{
value = await dataSourceInitializer.EnsureInitializedAsync(
Expand All @@ -52,17 +53,14 @@ internal static class PropertyDataResolver
context.MethodMetadata,
context.Events);
}
// Otherwise, register if it has injectable properties
else if (PropertyInjectionCache.HasInjectableProperties(value.GetType()))
else if (PropertyInjectionCache.HasInjectableProperties(value.GetType()) || value is IAsyncInitializer)
{
// Use ObjectRegistrationService for registration (property injection + tracking, NO IAsyncInitializer)
await objectRegistrationService.RegisterObjectAsync(
value = await dataSourceInitializer.EnsureInitializedAsync(
value,
context.ObjectBag,
context.MethodMetadata,
context.Events);
}
// Note: IAsyncInitializer will be called during execution phase by ObjectInitializationService

return value;
}
Expand Down

This file was deleted.

Loading
Loading