From c01a0006072f68ec1f7775a7327b78e73d7e52b1 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:30:43 +0100 Subject: [PATCH 1/4] refactor: move object initialization into centralised location --- .../StaticPropertyReflectionInitializer.cs | 6 - .../Strategies/NestedPropertyStrategy.cs | 10 -- .../PropertyInitializationOrchestrator.cs | 13 +- TUnit.Engine/TestInitializer.cs | 11 ++ TUnit.TestProject/Bugs/3266/Tests.cs | 133 ++++++++++++++++++ 5 files changed, 145 insertions(+), 28 deletions(-) create mode 100644 TUnit.TestProject/Bugs/3266/Tests.cs diff --git a/TUnit.Core/StaticPropertyReflectionInitializer.cs b/TUnit.Core/StaticPropertyReflectionInitializer.cs index 72bcca272c..aa31ab09ed 100644 --- a/TUnit.Core/StaticPropertyReflectionInitializer.cs +++ b/TUnit.Core/StaticPropertyReflectionInitializer.cs @@ -124,12 +124,6 @@ private static async Task InitializeStaticProperty(Type type, PropertyInfo prope // Set the property value property.SetValue(null, value); - // Initialize the value if it's an object - if (value != null) - { - await ObjectInitializer.InitializeAsync(value); - } - // Only use the first value for static properties break; } diff --git a/TUnit.Engine/Services/PropertyInitialization/Strategies/NestedPropertyStrategy.cs b/TUnit.Engine/Services/PropertyInitialization/Strategies/NestedPropertyStrategy.cs index a338122494..45be31469d 100644 --- a/TUnit.Engine/Services/PropertyInitialization/Strategies/NestedPropertyStrategy.cs +++ b/TUnit.Engine/Services/PropertyInitialization/Strategies/NestedPropertyStrategy.cs @@ -55,13 +55,6 @@ public async Task InitializePropertyAsync(PropertyInitializationContext context) // 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; - } - // Recursively inject properties into the nested object if (SourceRegistrar.IsEnabled) { @@ -71,9 +64,6 @@ public async Task InitializePropertyAsync(PropertyInitializationContext context) { await ProcessReflectionNestedProperties(context, propertyValue, plan); } - - // Initialize the object after all properties are set - await ObjectInitializer.InitializeAsync(propertyValue); } /// diff --git a/TUnit.Engine/Services/PropertyInitializationOrchestrator.cs b/TUnit.Engine/Services/PropertyInitializationOrchestrator.cs index cad96d5cef..87f93e11b4 100644 --- a/TUnit.Engine/Services/PropertyInitializationOrchestrator.cs +++ b/TUnit.Engine/Services/PropertyInitializationOrchestrator.cs @@ -94,13 +94,6 @@ public async Task InitializeObjectWithPropertiesAsync( 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 // Properties will be fully initialized (including nested initialization) by the strategies if (SourceRegistrar.IsEnabled) @@ -113,10 +106,6 @@ await InitializePropertiesAsync( await InitializePropertiesAsync( instance, plan.ReflectionProperties, objectBag, methodMetadata, events, visitedObjects); } - - // 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); } /// @@ -180,4 +169,4 @@ private PropertyInitializationContext CreateContext( /// /// Gets the singleton instance of the orchestrator. /// -} \ No newline at end of file +} diff --git a/TUnit.Engine/TestInitializer.cs b/TUnit.Engine/TestInitializer.cs index 1b3454853c..60050ccc75 100644 --- a/TUnit.Engine/TestInitializer.cs +++ b/TUnit.Engine/TestInitializer.cs @@ -56,5 +56,16 @@ await _propertyInjectionService.InjectPropertiesIntoObjectAsync( // Shouldn't retrack already tracked objects, but will track any new ones created during retries / initialization _objectTracker.TrackObjects(test.Context); + + // Initialize in reverse order (most nested first) + await Initialize(test.Context.TrackedObjects); + } + + private async Task Initialize(HashSet objects) + { + foreach (var obj in objects.Reverse()) + { + await ObjectInitializer.InitializeAsync(obj); + } } } diff --git a/TUnit.TestProject/Bugs/3266/Tests.cs b/TUnit.TestProject/Bugs/3266/Tests.cs new file mode 100644 index 0000000000..f18ef81aed --- /dev/null +++ b/TUnit.TestProject/Bugs/3266/Tests.cs @@ -0,0 +1,133 @@ +using TUnit.Core; +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs.Bug3266; + +// Mock test container - shared per test session +public class PulsarTestContainer : IAsyncInitializer, IAsyncDisposable +{ + public bool IsInitialized { get; private set; } + public bool IsDisposed { get; private set; } + + public Task InitializeAsync() + { + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() + { + IsDisposed = true; + return default; + } +} + +// Mock connection class - depends on PulsarTestContainer being initialized +public class PulsarConnection : IAsyncInitializer, IAsyncDisposable +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required PulsarTestContainer Container { get; init; } + + public bool IsInitialized { get; private set; } + public bool IsDisposed { get; private set; } + + public Task InitializeAsync() + { + // This should fail if Container.InitializeAsync() hasn't been called yet + if (!Container.IsInitialized) + { + throw new InvalidOperationException( + "PulsarConnection.InitializeAsync() called before nested Container property was initialized!"); + } + + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() + { + IsDisposed = true; + return default; + } +} + +// Mock web app factory - also depends on PulsarTestContainer +public class WebAppFactory : IAsyncInitializer, IAsyncDisposable +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required PulsarTestContainer Container { get; init; } + + public bool IsInitialized { get; private set; } + public bool IsDisposed { get; private set; } + + public Task InitializeAsync() + { + // This should fail if Container.InitializeAsync() hasn't been called yet + if (!Container.IsInitialized) + { + throw new InvalidOperationException( + "WebAppFactory.InitializeAsync() called before nested Container property was initialized!"); + } + + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() + { + IsDisposed = true; + return default; + } +} + +// Base abstract class with WebAppFactory property +public abstract class AbstractClassA +{ + [ClassDataSource(Shared = SharedType.None)] + public required WebAppFactory WebApp { get; init; } +} + +// Middle abstract class with PulsarConnection property +public abstract class AbstractClassB : AbstractClassA +{ + [ClassDataSource(Shared = SharedType.None)] + public required PulsarConnection Connection { get; init; } +} + +// Concrete test class - reproduces issue #3266 +[EngineTest(ExpectedResult.Pass)] +[NotInParallel] +public class Issue3266ReproTest : AbstractClassB +{ + [Test] + public async Task NestedPropertiesShouldBeInitializedBeforeParentInitializeAsync() + { + // Verify all nested containers are initialized + await Assert.That(Connection.Container.IsInitialized) + .IsTrue() + .Because("PulsarConnection's Container should be initialized"); + + await Assert.That(WebApp.Container.IsInitialized) + .IsTrue() + .Because("WebAppFactory's Container should be initialized"); + + // Verify parent objects are initialized + await Assert.That(Connection.IsInitialized) + .IsTrue() + .Because("PulsarConnection should be initialized"); + + await Assert.That(WebApp.IsInitialized) + .IsTrue() + .Because("WebAppFactory should be initialized"); + } + + [Test] + public async Task BothPropertiesShouldShareTheSameContainer() + { + // Since both use SharedType.PerTestSession, they should get the same instance + await Assert.That(Connection.Container) + .IsSameReferenceAs(WebApp.Container) + .Because("Both properties should share the same PulsarTestContainer instance"); + } +} From 28fad9f5f0a74db0322c14d831ff0cfc25e08068 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:33:07 +0100 Subject: [PATCH 2/4] refactor: rename method and improve clarity in object initialization --- TUnit.Engine/TestInitializer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/TUnit.Engine/TestInitializer.cs b/TUnit.Engine/TestInitializer.cs index 60050ccc75..360de3c37b 100644 --- a/TUnit.Engine/TestInitializer.cs +++ b/TUnit.Engine/TestInitializer.cs @@ -57,13 +57,13 @@ await _propertyInjectionService.InjectPropertiesIntoObjectAsync( // Shouldn't retrack already tracked objects, but will track any new ones created during retries / initialization _objectTracker.TrackObjects(test.Context); - // Initialize in reverse order (most nested first) - await Initialize(test.Context.TrackedObjects); + await InitializeTrackedObjects(test.Context); } - private async Task Initialize(HashSet objects) + private async Task InitializeTrackedObjects(TestContext testContext) { - foreach (var obj in objects.Reverse()) + // Initialize in reverse order (most nested first) + foreach (var obj in testContext.TrackedObjects.Reverse()) { await ObjectInitializer.InitializeAsync(obj); } From d029a641081b9b05332e9cd01028033d1ffd3975 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 2 Oct 2025 19:16:00 +0100 Subject: [PATCH 3/4] refactor: remove ObjectInitializationService and streamline test initialization process --- .idea/.idea.TUnit/.idea/workspace.xml | 18 ++- .../Framework/TUnitServiceProvider.cs | 4 +- .../Services/DataSourceInitializer.cs | 10 +- .../Services/ObjectInitializationService.cs | 127 ------------------ TUnit.Engine/TestInitializer.cs | 25 +--- 5 files changed, 20 insertions(+), 164 deletions(-) delete mode 100644 TUnit.Engine/Services/ObjectInitializationService.cs diff --git a/.idea/.idea.TUnit/.idea/workspace.xml b/.idea/.idea.TUnit/.idea/workspace.xml index 82352ed992..80e9f39bf6 100644 --- a/.idea/.idea.TUnit/.idea/workspace.xml +++ b/.idea/.idea.TUnit/.idea/workspace.xml @@ -54,15 +54,21 @@