diff --git a/TUnit.Core/Executors/GenericAbstractExecutor.cs b/TUnit.Core/Executors/GenericAbstractExecutor.cs index 8631771b9e..8be73248ba 100644 --- a/TUnit.Core/Executors/GenericAbstractExecutor.cs +++ b/TUnit.Core/Executors/GenericAbstractExecutor.cs @@ -57,6 +57,11 @@ public ValueTask ExecuteAfterTestHook(MethodMetadata hookMethodInfo, TestContext return ExecuteAsync(action); } + public ValueTask ExecuteDisposal(TestContext context, Func action) + { + return ExecuteAsync(action); + } + public ValueTask ExecuteTest(TestContext context, Func action) { return ExecuteAsync(action); diff --git a/TUnit.Core/Interfaces/IHookExecutor.cs b/TUnit.Core/Interfaces/IHookExecutor.cs index 6d3d1c8e81..13ec72d8fd 100644 --- a/TUnit.Core/Interfaces/IHookExecutor.cs +++ b/TUnit.Core/Interfaces/IHookExecutor.cs @@ -13,4 +13,11 @@ public interface IHookExecutor ValueTask ExecuteAfterAssemblyHook(MethodMetadata hookMethodInfo, AssemblyHookContext context, Func action); ValueTask ExecuteAfterClassHook(MethodMetadata hookMethodInfo, ClassHookContext context, Func action); ValueTask ExecuteAfterTestHook(MethodMetadata hookMethodInfo, TestContext context, Func action); + +#if NETSTANDARD2_0 + ValueTask ExecuteDisposal(TestContext context, Func action); +#else + ValueTask ExecuteDisposal(TestContext context, Func action) + => action(); +#endif } diff --git a/TUnit.Engine/Discovery/ReflectionHookDiscoveryService.cs b/TUnit.Engine/Discovery/ReflectionHookDiscoveryService.cs index cc3954b121..de062bd768 100644 --- a/TUnit.Engine/Discovery/ReflectionHookDiscoveryService.cs +++ b/TUnit.Engine/Discovery/ReflectionHookDiscoveryService.cs @@ -1029,4 +1029,7 @@ public ValueTask ExecuteBeforeTestSessionHook(MethodMetadata testMethod, TestSes public ValueTask ExecuteAfterTestSessionHook(MethodMetadata testMethod, TestSessionContext context, Func action) => action(); + + public ValueTask ExecuteDisposal(TestContext context, Func action) + => action(); } diff --git a/TUnit.Engine/Services/TestExecution/TestCoordinator.cs b/TUnit.Engine/Services/TestExecution/TestCoordinator.cs index c34181a332..928f603df0 100644 --- a/TUnit.Engine/Services/TestExecution/TestCoordinator.cs +++ b/TUnit.Engine/Services/TestExecution/TestCoordinator.cs @@ -1,6 +1,7 @@ using System.Linq; using TUnit.Core; using TUnit.Core.Exceptions; +using TUnit.Core.Interfaces; using TUnit.Core.Logging; using TUnit.Core.Tracking; using TUnit.Engine.Helpers; @@ -152,7 +153,8 @@ await TimeoutHelper.ExecuteWithTimeoutAsync( try { - await TestExecutor.DisposeTestInstance(test).ConfigureAwait(false); + var hookExecutor = test.Context.CustomHookExecutor; + await TestExecutor.DisposeTestInstance(test, hookExecutor).ConfigureAwait(false); } catch (Exception disposeEx) { diff --git a/TUnit.Engine/TestExecutor.cs b/TUnit.Engine/TestExecutor.cs index 18a3a21430..8418bcb322 100644 --- a/TUnit.Engine/TestExecutor.cs +++ b/TUnit.Engine/TestExecutor.cs @@ -247,12 +247,12 @@ public IContextProvider GetContextProvider() return _contextProvider; } - internal static async Task DisposeTestInstance(AbstractExecutableTest test) + internal static async Task DisposeTestInstance(AbstractExecutableTest test, IHookExecutor? hookExecutor = null) { // Dispose the test instance if it's disposable if (test.Context.Metadata.TestDetails.ClassInstance is not SkippedTestInstance) { - try + async ValueTask DisposeAsync() { var instance = test.Context.Metadata.TestDetails.ClassInstance; @@ -266,6 +266,18 @@ internal static async Task DisposeTestInstance(AbstractExecutableTest test) break; } } + + try + { + if (hookExecutor != null) + { + await hookExecutor.ExecuteDisposal(test.Context, DisposeAsync).ConfigureAwait(false); + } + else + { + await DisposeAsync().ConfigureAwait(false); + } + } catch { // Swallow disposal errors - they shouldn't fail the test diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 556486d6e4..8dd30a5468 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -749,6 +749,7 @@ namespace public . ExecuteBeforeTestDiscoveryHook(.MethodMetadata hookMethodInfo, .BeforeTestDiscoveryContext context, <.> action) { } public . ExecuteBeforeTestHook(.MethodMetadata hookMethodInfo, .TestContext context, <.> action) { } public . ExecuteBeforeTestSessionHook(.MethodMetadata hookMethodInfo, .TestSessionContext context, <.> action) { } + public . ExecuteDisposal(.TestContext context, <.> action) { } public . ExecuteTest(.TestContext context, <.> action) { } } public sealed class GenericMethodInfo @@ -2269,6 +2270,7 @@ namespace .Interfaces . ExecuteBeforeTestDiscoveryHook(.MethodMetadata hookMethodInfo, .BeforeTestDiscoveryContext context, <.> action); . ExecuteBeforeTestHook(.MethodMetadata hookMethodInfo, .TestContext context, <.> action); . ExecuteBeforeTestSessionHook(.MethodMetadata hookMethodInfo, .TestSessionContext context, <.> action); + . ExecuteDisposal(.TestContext context, <.> action); } public interface IHookRegisteredEventReceiver : . { 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 8bbca55ff3..917f3ce1f2 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 @@ -749,6 +749,7 @@ namespace public . ExecuteBeforeTestDiscoveryHook(.MethodMetadata hookMethodInfo, .BeforeTestDiscoveryContext context, <.> action) { } public . ExecuteBeforeTestHook(.MethodMetadata hookMethodInfo, .TestContext context, <.> action) { } public . ExecuteBeforeTestSessionHook(.MethodMetadata hookMethodInfo, .TestSessionContext context, <.> action) { } + public . ExecuteDisposal(.TestContext context, <.> action) { } public . ExecuteTest(.TestContext context, <.> action) { } } public sealed class GenericMethodInfo @@ -2269,6 +2270,7 @@ namespace .Interfaces . ExecuteBeforeTestDiscoveryHook(.MethodMetadata hookMethodInfo, .BeforeTestDiscoveryContext context, <.> action); . ExecuteBeforeTestHook(.MethodMetadata hookMethodInfo, .TestContext context, <.> action); . ExecuteBeforeTestSessionHook(.MethodMetadata hookMethodInfo, .TestSessionContext context, <.> action); + . ExecuteDisposal(.TestContext context, <.> action); } public interface IHookRegisteredEventReceiver : . { 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 e7945a9846..dc18aa6f69 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 @@ -749,6 +749,7 @@ namespace public . ExecuteBeforeTestDiscoveryHook(.MethodMetadata hookMethodInfo, .BeforeTestDiscoveryContext context, <.> action) { } public . ExecuteBeforeTestHook(.MethodMetadata hookMethodInfo, .TestContext context, <.> action) { } public . ExecuteBeforeTestSessionHook(.MethodMetadata hookMethodInfo, .TestSessionContext context, <.> action) { } + public . ExecuteDisposal(.TestContext context, <.> action) { } public . ExecuteTest(.TestContext context, <.> action) { } } public sealed class GenericMethodInfo @@ -2269,6 +2270,7 @@ namespace .Interfaces . ExecuteBeforeTestDiscoveryHook(.MethodMetadata hookMethodInfo, .BeforeTestDiscoveryContext context, <.> action); . ExecuteBeforeTestHook(.MethodMetadata hookMethodInfo, .TestContext context, <.> action); . ExecuteBeforeTestSessionHook(.MethodMetadata hookMethodInfo, .TestSessionContext context, <.> action); + . ExecuteDisposal(.TestContext context, <.> action); } public interface IHookRegisteredEventReceiver : . { 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 dd3923472e..7cfcb2a8fc 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 @@ -726,6 +726,7 @@ namespace public . ExecuteBeforeTestDiscoveryHook(.MethodMetadata hookMethodInfo, .BeforeTestDiscoveryContext context, <.> action) { } public . ExecuteBeforeTestHook(.MethodMetadata hookMethodInfo, .TestContext context, <.> action) { } public . ExecuteBeforeTestSessionHook(.MethodMetadata hookMethodInfo, .TestSessionContext context, <.> action) { } + public . ExecuteDisposal(.TestContext context, <.> action) { } public . ExecuteTest(.TestContext context, <.> action) { } } public sealed class GenericMethodInfo @@ -2201,6 +2202,7 @@ namespace .Interfaces . ExecuteBeforeTestDiscoveryHook(.MethodMetadata hookMethodInfo, .BeforeTestDiscoveryContext context, <.> action); . ExecuteBeforeTestHook(.MethodMetadata hookMethodInfo, .TestContext context, <.> action); . ExecuteBeforeTestSessionHook(.MethodMetadata hookMethodInfo, .TestSessionContext context, <.> action); + . ExecuteDisposal(.TestContext context, <.> action); } public interface IHookRegisteredEventReceiver : . { diff --git a/TUnit.TestProject/SetHookExecutorTests.cs b/TUnit.TestProject/SetHookExecutorTests.cs index 5d11036a32..dc2427011b 100644 --- a/TUnit.TestProject/SetHookExecutorTests.cs +++ b/TUnit.TestProject/SetHookExecutorTests.cs @@ -117,3 +117,36 @@ public async Task Test_StaticHooksExecuteInCustomExecutor() await Assert.That(Thread.CurrentThread.Name).IsEqualTo("CrossPlatformTestExecutor"); } } + +/// +/// Tests demonstrating SetHookExecutor affects disposal execution - Issue #3918 +/// +[EngineTest(ExpectedResult.Pass)] +[SetBothExecutors] // This attribute sets both executors +public class DisposalWithHookExecutorTests : IAsyncDisposable +{ + private static bool _disposalExecutedInCustomExecutor; + + [Test] + public async Task Test_ExecutesInCustomExecutor() + { + // Test should execute in custom executor + await Assert.That(Thread.CurrentThread.Name).IsEqualTo("CrossPlatformTestExecutor"); + await Assert.That(CrossPlatformTestExecutor.IsRunningInTestExecutor.Value).IsTrue(); + } + + public ValueTask DisposeAsync() + { + // Verify disposal runs in the custom executor + _disposalExecutedInCustomExecutor = + Thread.CurrentThread.Name == "CrossPlatformTestExecutor" && + CrossPlatformTestExecutor.IsRunningInTestExecutor.Value; + return default; + } + + [After(Class)] + public static async Task VerifyDisposalRanInCustomExecutor(ClassHookContext context) + { + await Assert.That(_disposalExecutedInCustomExecutor).IsTrue(); + } +}