diff --git a/TUnit.Engine/Interfaces/IHookDelegateBuilder.cs b/TUnit.Engine/Interfaces/IHookDelegateBuilder.cs index 13354c314f..1a99cc7c90 100644 --- a/TUnit.Engine/Interfaces/IHookDelegateBuilder.cs +++ b/TUnit.Engine/Interfaces/IHookDelegateBuilder.cs @@ -14,24 +14,24 @@ internal interface IHookDelegateBuilder /// ValueTask InitializeAsync(); - ValueTask>> CollectBeforeTestHooksAsync(Type testClassType); - ValueTask>> CollectAfterTestHooksAsync(Type testClassType); - ValueTask>> CollectBeforeEveryTestHooksAsync(Type testClassType); - ValueTask>> CollectAfterEveryTestHooksAsync(Type testClassType); + ValueTask>> CollectBeforeTestHooksAsync(Type testClassType); + ValueTask>> CollectAfterTestHooksAsync(Type testClassType); + ValueTask>> CollectBeforeEveryTestHooksAsync(Type testClassType); + ValueTask>> CollectAfterEveryTestHooksAsync(Type testClassType); - ValueTask>> CollectBeforeClassHooksAsync(Type testClassType); - ValueTask>> CollectAfterClassHooksAsync(Type testClassType); - ValueTask>> CollectBeforeEveryClassHooksAsync(); - ValueTask>> CollectAfterEveryClassHooksAsync(); + ValueTask>> CollectBeforeClassHooksAsync(Type testClassType); + ValueTask>> CollectAfterClassHooksAsync(Type testClassType); + ValueTask>> CollectBeforeEveryClassHooksAsync(); + ValueTask>> CollectAfterEveryClassHooksAsync(); - ValueTask>> CollectBeforeAssemblyHooksAsync(Assembly assembly); - ValueTask>> CollectAfterAssemblyHooksAsync(Assembly assembly); - ValueTask>> CollectBeforeEveryAssemblyHooksAsync(); - ValueTask>> CollectAfterEveryAssemblyHooksAsync(); + ValueTask>> CollectBeforeAssemblyHooksAsync(Assembly assembly); + ValueTask>> CollectAfterAssemblyHooksAsync(Assembly assembly); + ValueTask>> CollectBeforeEveryAssemblyHooksAsync(); + ValueTask>> CollectAfterEveryAssemblyHooksAsync(); - ValueTask>> CollectBeforeTestSessionHooksAsync(); - ValueTask>> CollectAfterTestSessionHooksAsync(); + ValueTask>> CollectBeforeTestSessionHooksAsync(); + ValueTask>> CollectAfterTestSessionHooksAsync(); - ValueTask>> CollectBeforeTestDiscoveryHooksAsync(); - ValueTask>> CollectAfterTestDiscoveryHooksAsync(); + ValueTask>> CollectBeforeTestDiscoveryHooksAsync(); + ValueTask>> CollectAfterTestDiscoveryHooksAsync(); } diff --git a/TUnit.Engine/NamedHookDelegate.cs b/TUnit.Engine/NamedHookDelegate.cs new file mode 100644 index 0000000000..a79e0f399d --- /dev/null +++ b/TUnit.Engine/NamedHookDelegate.cs @@ -0,0 +1,12 @@ +namespace TUnit.Engine; + +/// +/// Wraps a hook delegate with its name for activity/span creation. +/// +internal readonly record struct NamedHookDelegate(string Name, string ActivityName, Func Invoke) +{ + public NamedHookDelegate(string name, Func invoke) + : this(name, $"hook: {name}", invoke) + { + } +} diff --git a/TUnit.Engine/Services/HookDelegateBuilder.cs b/TUnit.Engine/Services/HookDelegateBuilder.cs index 0f5189d9b1..e367e7a347 100644 --- a/TUnit.Engine/Services/HookDelegateBuilder.cs +++ b/TUnit.Engine/Services/HookDelegateBuilder.cs @@ -17,27 +17,27 @@ internal sealed class HookDelegateBuilder : IHookDelegateBuilder { private readonly EventReceiverOrchestrator _eventReceiverOrchestrator; private readonly TUnitFrameworkLogger _logger; - private readonly ConcurrentDictionary>> _beforeTestHooksCache = new(); - private readonly ConcurrentDictionary>> _afterTestHooksCache = new(); - private readonly ConcurrentDictionary>> _beforeClassHooksCache = new(); - private readonly ConcurrentDictionary>> _afterClassHooksCache = new(); - private readonly ConcurrentDictionary>> _beforeAssemblyHooksCache = new(); - private readonly ConcurrentDictionary>> _afterAssemblyHooksCache = new(); + private readonly ConcurrentDictionary>> _beforeTestHooksCache = new(); + private readonly ConcurrentDictionary>> _afterTestHooksCache = new(); + private readonly ConcurrentDictionary>> _beforeClassHooksCache = new(); + private readonly ConcurrentDictionary>> _afterClassHooksCache = new(); + private readonly ConcurrentDictionary>> _beforeAssemblyHooksCache = new(); + private readonly ConcurrentDictionary>> _afterAssemblyHooksCache = new(); // Cache for GetGenericTypeDefinition() calls to avoid repeated reflection private static readonly ConcurrentDictionary _genericTypeDefinitionCache = new(); // Pre-computed global hooks (computed once at initialization) - private IReadOnlyList>? _beforeEveryTestHooks; - private IReadOnlyList>? _afterEveryTestHooks; - private IReadOnlyList>? _beforeTestSessionHooks; - private IReadOnlyList>? _afterTestSessionHooks; - private IReadOnlyList>? _beforeTestDiscoveryHooks; - private IReadOnlyList>? _afterTestDiscoveryHooks; - private IReadOnlyList>? _beforeEveryClassHooks; - private IReadOnlyList>? _afterEveryClassHooks; - private IReadOnlyList>? _beforeEveryAssemblyHooks; - private IReadOnlyList>? _afterEveryAssemblyHooks; + private IReadOnlyList>? _beforeEveryTestHooks; + private IReadOnlyList>? _afterEveryTestHooks; + private IReadOnlyList>? _beforeTestSessionHooks; + private IReadOnlyList>? _afterTestSessionHooks; + private IReadOnlyList>? _beforeTestDiscoveryHooks; + private IReadOnlyList>? _afterTestDiscoveryHooks; + private IReadOnlyList>? _beforeEveryClassHooks; + private IReadOnlyList>? _afterEveryClassHooks; + private IReadOnlyList>? _beforeEveryAssemblyHooks; + private IReadOnlyList>? _afterEveryAssemblyHooks; // Cache for processed hooks to avoid re-processing event receivers private readonly ConcurrentDictionary _processedHooks = new(); @@ -82,21 +82,21 @@ public async ValueTask InitializeAsync() /// Generic helper to build global hooks from Sources collections. /// Eliminates duplication across all BuildGlobalXXXHooksAsync methods. /// - private async Task>> BuildGlobalHooksAsync( + private async Task>> BuildGlobalHooksAsync( IEnumerable sourceHooks, - Func>> createDelegate, + Func>> createDelegate, string hookTypeName) where THookMethod : HookMethod { // Pre-size list if possible for better performance var capacity = sourceHooks is ICollection coll ? coll.Count : 0; - var hooks = new List<(int order, int registrationIndex, Func hook)>(capacity); + var hooks = new List<(int order, int registrationIndex, NamedHookDelegate hook)>(capacity); foreach (var hook in sourceHooks) { await _logger.LogTraceAsync($"Creating delegate for {hookTypeName} hook: {hook.Name}").ConfigureAwait(false); - var hookFunc = await createDelegate(hook); - hooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); + var namedHook = await createDelegate(hook); + hooks.Add((hook.Order, hook.RegistrationIndex, namedHook)); } if (hooks.Count > 0) @@ -111,54 +111,39 @@ private async Task>> Build .ToList(); } - private Task>> BuildGlobalBeforeEveryTestHooksAsync() + private Task>> BuildGlobalBeforeEveryTestHooksAsync() => BuildGlobalHooksAsync(Sources.BeforeEveryTestHooks, CreateStaticHookDelegateAsync, "BeforeEveryTest"); - private Task>> BuildGlobalAfterEveryTestHooksAsync() + private Task>> BuildGlobalAfterEveryTestHooksAsync() => BuildGlobalHooksAsync(Sources.AfterEveryTestHooks, CreateStaticHookDelegateAsync, "AfterEveryTest"); - private Task>> BuildGlobalBeforeTestSessionHooksAsync() - => BuildGlobalHooksAsync(Sources.BeforeTestSessionHooks, CreateTestSessionHookDelegateAsync, "BeforeTestSession"); + private Task>> BuildGlobalBeforeTestSessionHooksAsync() + => BuildGlobalHooksAsync(Sources.BeforeTestSessionHooks, CreateStaticHookDelegateAsync, "BeforeTestSession"); - private Task>> BuildGlobalAfterTestSessionHooksAsync() - => BuildGlobalHooksAsync(Sources.AfterTestSessionHooks, CreateTestSessionHookDelegateAsync, "AfterTestSession"); + private Task>> BuildGlobalAfterTestSessionHooksAsync() + => BuildGlobalHooksAsync(Sources.AfterTestSessionHooks, CreateStaticHookDelegateAsync, "AfterTestSession"); - private Task>> BuildGlobalBeforeTestDiscoveryHooksAsync() - => BuildGlobalHooksAsync(Sources.BeforeTestDiscoveryHooks, CreateBeforeTestDiscoveryHookDelegateAsync, "BeforeTestDiscovery"); + private Task>> BuildGlobalBeforeTestDiscoveryHooksAsync() + => BuildGlobalHooksAsync(Sources.BeforeTestDiscoveryHooks, CreateStaticHookDelegateAsync, "BeforeTestDiscovery"); - private Task>> BuildGlobalAfterTestDiscoveryHooksAsync() - => BuildGlobalHooksAsync(Sources.AfterTestDiscoveryHooks, CreateTestDiscoveryHookDelegateAsync, "AfterTestDiscovery"); + private Task>> BuildGlobalAfterTestDiscoveryHooksAsync() + => BuildGlobalHooksAsync(Sources.AfterTestDiscoveryHooks, CreateStaticHookDelegateAsync, "AfterTestDiscovery"); - private Task>> BuildGlobalBeforeEveryClassHooksAsync() - => BuildGlobalHooksAsync(Sources.BeforeEveryClassHooks, CreateClassHookDelegateAsync, "BeforeEveryClass"); + private Task>> BuildGlobalBeforeEveryClassHooksAsync() + => BuildGlobalHooksAsync(Sources.BeforeEveryClassHooks, CreateStaticHookDelegateAsync, "BeforeEveryClass"); - private Task>> BuildGlobalAfterEveryClassHooksAsync() - => BuildGlobalHooksAsync(Sources.AfterEveryClassHooks, CreateClassHookDelegateAsync, "AfterEveryClass"); + private Task>> BuildGlobalAfterEveryClassHooksAsync() + => BuildGlobalHooksAsync(Sources.AfterEveryClassHooks, CreateStaticHookDelegateAsync, "AfterEveryClass"); - private Task>> BuildGlobalBeforeEveryAssemblyHooksAsync() - => BuildGlobalHooksAsync(Sources.BeforeEveryAssemblyHooks, CreateAssemblyHookDelegateAsync, "BeforeEveryAssembly"); + private Task>> BuildGlobalBeforeEveryAssemblyHooksAsync() + => BuildGlobalHooksAsync(Sources.BeforeEveryAssemblyHooks, CreateStaticHookDelegateAsync, "BeforeEveryAssembly"); - private Task>> BuildGlobalAfterEveryAssemblyHooksAsync() - => BuildGlobalHooksAsync(Sources.AfterEveryAssemblyHooks, CreateAssemblyHookDelegateAsync, "AfterEveryAssembly"); + private Task>> BuildGlobalAfterEveryAssemblyHooksAsync() + => BuildGlobalHooksAsync(Sources.AfterEveryAssemblyHooks, CreateStaticHookDelegateAsync, "AfterEveryAssembly"); - private static void SortAndAddHooks( - List> target, - List<(int order, int registrationIndex, TDelegate hook)> hooks) - where TDelegate : Delegate - { - hooks.Sort((a, b) => a.order != b.order - ? a.order.CompareTo(b.order) - : a.registrationIndex.CompareTo(b.registrationIndex)); - - foreach (var (_, _, hook) in hooks) - { - target.Add((Func)(object)hook); - } - } - - private static void SortAndAddClassHooks( - List> target, - List<(int order, int registrationIndex, Func hook)> hooks) + private static void SortAndAddHooks( + List> target, + List<(int order, int registrationIndex, NamedHookDelegate hook)> hooks) { hooks.Sort((a, b) => a.order != b.order ? a.order.CompareTo(b.order) @@ -190,7 +175,7 @@ private async Task ProcessHookRegistrationAsync(HookMethod hookMethod, Cancellat } } - public async ValueTask>> CollectBeforeTestHooksAsync(Type testClassType) + public async ValueTask>> CollectBeforeTestHooksAsync(Type testClassType) { if (_beforeTestHooksCache.TryGetValue(testClassType, out var cachedHooks)) { @@ -202,22 +187,22 @@ public async ValueTask> return hooks; } - private async Task>> BuildBeforeTestHooksAsync(Type type) + private async Task>> BuildBeforeTestHooksAsync(Type type) { - var hooksByType = new List<(Type type, List<(int order, int registrationIndex, Func hook)> hooks)>(); + var hooksByType = new List<(Type type, List<(int order, int registrationIndex, NamedHookDelegate hook)> hooks)>(); // Collect hooks for each type in the hierarchy var currentType = type; while (currentType != null) { - var typeHooks = new List<(int order, int registrationIndex, Func hook)>(); + var typeHooks = new List<(int order, int registrationIndex, NamedHookDelegate hook)>(); if (Sources.BeforeTestHooks.TryGetValue(currentType, out var sourceHooks)) { foreach (var hook in sourceHooks) { - var hookFunc = await CreateInstanceHookDelegateAsync(hook); - typeHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); + var namedHook = await CreateInstanceHookDelegateAsync(hook); + typeHooks.Add((hook.Order, hook.RegistrationIndex, namedHook)); } } @@ -229,8 +214,8 @@ private async Task>> Bu { foreach (var hook in openTypeHooks) { - var hookFunc = await CreateInstanceHookDelegateAsync(hook); - typeHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); + var namedHook = await CreateInstanceHookDelegateAsync(hook); + typeHooks.Add((hook.Order, hook.RegistrationIndex, namedHook)); } } } @@ -247,7 +232,7 @@ private async Task>> Bu // Reverse the list since we collected from derived to base hooksByType.Reverse(); - var finalHooks = new List>(); + var finalHooks = new List>(); foreach (var (_, typeHooks) in hooksByType) { // Within each type level, sort by Order then by RegistrationIndex @@ -257,7 +242,7 @@ private async Task>> Bu return finalHooks; } - public async ValueTask>> CollectAfterTestHooksAsync(Type testClassType) + public async ValueTask>> CollectAfterTestHooksAsync(Type testClassType) { if (_afterTestHooksCache.TryGetValue(testClassType, out var cachedHooks)) { @@ -269,22 +254,22 @@ public async ValueTask> return hooks; } - private async Task>> BuildAfterTestHooksAsync(Type type) + private async Task>> BuildAfterTestHooksAsync(Type type) { - var hooksByType = new List<(Type type, List<(int order, int registrationIndex, Func hook)> hooks)>(); + var hooksByType = new List<(Type type, List<(int order, int registrationIndex, NamedHookDelegate hook)> hooks)>(); // Collect hooks for each type in the hierarchy var currentType = type; while (currentType != null) { - var typeHooks = new List<(int order, int registrationIndex, Func hook)>(); + var typeHooks = new List<(int order, int registrationIndex, NamedHookDelegate hook)>(); if (Sources.AfterTestHooks.TryGetValue(currentType, out var sourceHooks)) { foreach (var hook in sourceHooks) { - var hookFunc = await CreateInstanceHookDelegateAsync(hook); - typeHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); + var namedHook = await CreateInstanceHookDelegateAsync(hook); + typeHooks.Add((hook.Order, hook.RegistrationIndex, namedHook)); } } @@ -296,8 +281,8 @@ private async Task>> Bu { foreach (var hook in openTypeHooks) { - var hookFunc = await CreateInstanceHookDelegateAsync(hook); - typeHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); + var namedHook = await CreateInstanceHookDelegateAsync(hook); + typeHooks.Add((hook.Order, hook.RegistrationIndex, namedHook)); } } } @@ -313,7 +298,7 @@ private async Task>> Bu // For After hooks: derived class hooks run first // No need to reverse since we collected from derived to base - var finalHooks = new List>(); + var finalHooks = new List>(); foreach (var (_, typeHooks) in hooksByType) { // Within each type level, sort by Order then by RegistrationIndex @@ -323,17 +308,17 @@ private async Task>> Bu return finalHooks; } - public ValueTask>> CollectBeforeEveryTestHooksAsync(Type testClassType) + public ValueTask>> CollectBeforeEveryTestHooksAsync(Type testClassType) { - return new ValueTask>>(_beforeEveryTestHooks ?? []); + return new ValueTask>>(_beforeEveryTestHooks ?? []); } - public ValueTask>> CollectAfterEveryTestHooksAsync(Type testClassType) + public ValueTask>> CollectAfterEveryTestHooksAsync(Type testClassType) { - return new ValueTask>>(_afterEveryTestHooks ?? []); + return new ValueTask>>(_afterEveryTestHooks ?? []); } - public async ValueTask>> CollectBeforeClassHooksAsync(Type testClassType) + public async ValueTask>> CollectBeforeClassHooksAsync(Type testClassType) { if (_beforeClassHooksCache.TryGetValue(testClassType, out var cachedHooks)) { @@ -345,22 +330,22 @@ public async ValueTask>> BuildBeforeClassHooksAsync(Type type) + private async Task>> BuildBeforeClassHooksAsync(Type type) { - var hooksByType = new List<(Type type, List<(int order, int registrationIndex, Func hook)> hooks)>(); + var hooksByType = new List<(Type type, List<(int order, int registrationIndex, NamedHookDelegate hook)> hooks)>(); // Collect hooks for each type in the hierarchy var currentType = type; while (currentType != null) { - var typeHooks = new List<(int order, int registrationIndex, Func hook)>(); + var typeHooks = new List<(int order, int registrationIndex, NamedHookDelegate hook)>(); if (Sources.BeforeClassHooks.TryGetValue(currentType, out var sourceHooks)) { foreach (var hook in sourceHooks) { - var hookFunc = await CreateClassHookDelegateAsync(hook); - typeHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); + var namedHook = await CreateStaticHookDelegateAsync(hook); + typeHooks.Add((hook.Order, hook.RegistrationIndex, namedHook)); } } @@ -372,8 +357,8 @@ private async Task { foreach (var hook in openTypeHooks) { - var hookFunc = await CreateClassHookDelegateAsync(hook); - typeHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); + var namedHook = await CreateStaticHookDelegateAsync(hook); + typeHooks.Add((hook.Order, hook.RegistrationIndex, namedHook)); } } } @@ -388,16 +373,16 @@ private async Task hooksByType.Reverse(); - var finalHooks = new List>(); + var finalHooks = new List>(); foreach (var (_, typeHooks) in hooksByType) { - SortAndAddClassHooks(finalHooks, typeHooks); + SortAndAddHooks(finalHooks, typeHooks); } return finalHooks; } - public async ValueTask>> CollectAfterClassHooksAsync(Type testClassType) + public async ValueTask>> CollectAfterClassHooksAsync(Type testClassType) { if (_afterClassHooksCache.TryGetValue(testClassType, out var cachedHooks)) { @@ -409,22 +394,22 @@ public async ValueTask>> BuildAfterClassHooksAsync(Type type) + private async Task>> BuildAfterClassHooksAsync(Type type) { - var hooksByType = new List<(Type type, List<(int order, int registrationIndex, Func hook)> hooks)>(); + var hooksByType = new List<(Type type, List<(int order, int registrationIndex, NamedHookDelegate hook)> hooks)>(); // Collect hooks for each type in the hierarchy var currentType = type; while (currentType != null) { - var typeHooks = new List<(int order, int registrationIndex, Func hook)>(); + var typeHooks = new List<(int order, int registrationIndex, NamedHookDelegate hook)>(); if (Sources.AfterClassHooks.TryGetValue(currentType, out var sourceHooks)) { foreach (var hook in sourceHooks) { - var hookFunc = await CreateClassHookDelegateAsync(hook); - typeHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); + var namedHook = await CreateStaticHookDelegateAsync(hook); + typeHooks.Add((hook.Order, hook.RegistrationIndex, namedHook)); } } @@ -436,8 +421,8 @@ private async Task { foreach (var hook in openTypeHooks) { - var hookFunc = await CreateClassHookDelegateAsync(hook); - typeHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); + var namedHook = await CreateStaticHookDelegateAsync(hook); + typeHooks.Add((hook.Order, hook.RegistrationIndex, namedHook)); } } } @@ -450,16 +435,16 @@ private async Task currentType = currentType.BaseType; } - var finalHooks = new List>(); + var finalHooks = new List>(); foreach (var (_, typeHooks) in hooksByType) { - SortAndAddClassHooks(finalHooks, typeHooks); + SortAndAddHooks(finalHooks, typeHooks); } return finalHooks; } - public async ValueTask>> CollectBeforeAssemblyHooksAsync(Assembly assembly) + public async ValueTask>> CollectBeforeAssemblyHooksAsync(Assembly assembly) { if (_beforeAssemblyHooksCache.TryGetValue(assembly, out var cachedHooks)) { @@ -471,28 +456,29 @@ public async ValueTask>> BuildBeforeAssemblyHooksAsync(Assembly assembly) + private async Task>> BuildBeforeAssemblyHooksAsync(Assembly assembly) { if (!Sources.BeforeAssemblyHooks.TryGetValue(assembly, out var assemblyHooks)) { return []; } - var allHooks = new List<(int order, Func hook)>(assemblyHooks.Count); + var allHooks = new List<(int order, int registrationIndex, NamedHookDelegate hook)>(assemblyHooks.Count); foreach (var hook in assemblyHooks) { - var hookFunc = await CreateAssemblyHookDelegateAsync(hook); - allHooks.Add((hook.Order, hookFunc)); + var namedHook = await CreateStaticHookDelegateAsync(hook); + allHooks.Add((hook.Order, hook.RegistrationIndex, namedHook)); } return allHooks - .OrderBy(h => h.order) - .Select(h => h.hook) + .OrderBy(static h => h.order) + .ThenBy(static h => h.registrationIndex) + .Select(static h => h.hook) .ToList(); } - public async ValueTask>> CollectAfterAssemblyHooksAsync(Assembly assembly) + public async ValueTask>> CollectAfterAssemblyHooksAsync(Assembly assembly) { if (_afterAssemblyHooksCache.TryGetValue(assembly, out var cachedHooks)) { @@ -504,73 +490,76 @@ public async ValueTask>> BuildAfterAssemblyHooksAsync(Assembly assembly) + private async Task>> BuildAfterAssemblyHooksAsync(Assembly assembly) { if (!Sources.AfterAssemblyHooks.TryGetValue(assembly, out var assemblyHooks)) { return []; } - var allHooks = new List<(int order, Func hook)>(assemblyHooks.Count); + var allHooks = new List<(int order, int registrationIndex, NamedHookDelegate hook)>(assemblyHooks.Count); foreach (var hook in assemblyHooks) { - var hookFunc = await CreateAssemblyHookDelegateAsync(hook); - allHooks.Add((hook.Order, hookFunc)); + var namedHook = await CreateStaticHookDelegateAsync(hook); + allHooks.Add((hook.Order, hook.RegistrationIndex, namedHook)); } return allHooks - .OrderBy(h => h.order) - .Select(h => h.hook) + .OrderBy(static h => h.order) + .ThenBy(static h => h.registrationIndex) + .Select(static h => h.hook) .ToList(); } - public ValueTask>> CollectBeforeTestSessionHooksAsync() + public ValueTask>> CollectBeforeTestSessionHooksAsync() { - return new ValueTask>>(_beforeTestSessionHooks ?? []); + return new ValueTask>>(_beforeTestSessionHooks ?? []); } - public ValueTask>> CollectAfterTestSessionHooksAsync() + public ValueTask>> CollectAfterTestSessionHooksAsync() { - return new ValueTask>>(_afterTestSessionHooks ?? []); + return new ValueTask>>(_afterTestSessionHooks ?? []); } - public ValueTask>> CollectBeforeTestDiscoveryHooksAsync() + public ValueTask>> CollectBeforeTestDiscoveryHooksAsync() { - return new ValueTask>>(_beforeTestDiscoveryHooks ?? []); + return new ValueTask>>(_beforeTestDiscoveryHooks ?? []); } - public ValueTask>> CollectAfterTestDiscoveryHooksAsync() + public ValueTask>> CollectAfterTestDiscoveryHooksAsync() { - return new ValueTask>>(_afterTestDiscoveryHooks ?? []); + return new ValueTask>>(_afterTestDiscoveryHooks ?? []); } - public ValueTask>> CollectBeforeEveryClassHooksAsync() + public ValueTask>> CollectBeforeEveryClassHooksAsync() { - return new ValueTask>>(_beforeEveryClassHooks ?? []); + return new ValueTask>>(_beforeEveryClassHooks ?? []); } - public ValueTask>> CollectAfterEveryClassHooksAsync() + public ValueTask>> CollectAfterEveryClassHooksAsync() { - return new ValueTask>>(_afterEveryClassHooks ?? []); + return new ValueTask>>(_afterEveryClassHooks ?? []); } - public ValueTask>> CollectBeforeEveryAssemblyHooksAsync() + public ValueTask>> CollectBeforeEveryAssemblyHooksAsync() { - return new ValueTask>>(_beforeEveryAssemblyHooks ?? []); + return new ValueTask>>(_beforeEveryAssemblyHooks ?? []); } - public ValueTask>> CollectAfterEveryAssemblyHooksAsync() + public ValueTask>> CollectAfterEveryAssemblyHooksAsync() { - return new ValueTask>>(_afterEveryAssemblyHooks ?? []); + return new ValueTask>>(_afterEveryAssemblyHooks ?? []); } - private async Task> CreateInstanceHookDelegateAsync(InstanceHookMethod hook) + private async ValueTask> CreateInstanceHookDelegateAsync(InstanceHookMethod hook) { // Process hook registration event receivers await ProcessHookRegistrationAsync(hook); - return async (context, cancellationToken) => + var name = hook.Name; + + return new NamedHookDelegate(name, async (context, cancellationToken) => { // Check at EXECUTION time if a custom executor should be used if (context.CustomHookExecutor != null) @@ -607,142 +596,14 @@ await customExecutor.ExecuteBeforeTestHook( await timeoutAction(); } - }; + }); } - private async Task> CreateStaticHookDelegateAsync(StaticHookMethod hook) + private async ValueTask> CreateStaticHookDelegateAsync(StaticHookMethod hook) { - // Process hook registration event receivers await ProcessHookRegistrationAsync(hook); - return (context, cancellationToken) => HookTimeoutHelper.CreateTimeoutHookAction( - hook, - context, - cancellationToken); - } - - private static Func CreateClassHookDelegate(StaticHookMethod hook) - { - return (context, cancellationToken) => - { - return HookTimeoutHelper.CreateTimeoutHookAction( - hook, - context, - cancellationToken); - }; - } - - private async Task> CreateClassHookDelegateAsync(StaticHookMethod hook) - { - // Process hook registration event receivers - await ProcessHookRegistrationAsync(hook); - - return (context, cancellationToken) => - { - return HookTimeoutHelper.CreateTimeoutHookAction( - hook, - context, - cancellationToken); - }; - } - - private static Func CreateAssemblyHookDelegate(StaticHookMethod hook) - { - return (context, cancellationToken) => - { - return HookTimeoutHelper.CreateTimeoutHookAction( - hook, - context, - cancellationToken); - }; - } - - private async Task> CreateAssemblyHookDelegateAsync(StaticHookMethod hook) - { - // Process hook registration event receivers - await ProcessHookRegistrationAsync(hook); - - return (context, cancellationToken) => - { - return HookTimeoutHelper.CreateTimeoutHookAction( - hook, - context, - cancellationToken); - }; - } - - private static Func CreateTestSessionHookDelegate(StaticHookMethod hook) - { - return (context, cancellationToken) => - { - return HookTimeoutHelper.CreateTimeoutHookAction( - hook, - context, - cancellationToken); - }; - } - - private async Task> CreateTestSessionHookDelegateAsync(StaticHookMethod hook) - { - // Process hook registration event receivers - await ProcessHookRegistrationAsync(hook); - - return (context, cancellationToken) => - { - return HookTimeoutHelper.CreateTimeoutHookAction( - hook, - context, - cancellationToken); - }; - } - - private static Func CreateBeforeTestDiscoveryHookDelegate(StaticHookMethod hook) - { - return (context, cancellationToken) => - { - return HookTimeoutHelper.CreateTimeoutHookAction( - hook, - context, - cancellationToken); - }; - } - - private async Task> CreateBeforeTestDiscoveryHookDelegateAsync(StaticHookMethod hook) - { - // Process hook registration event receivers - await ProcessHookRegistrationAsync(hook); - - return (context, cancellationToken) => - { - return HookTimeoutHelper.CreateTimeoutHookAction( - hook, - context, - cancellationToken); - }; - } - - private static Func CreateTestDiscoveryHookDelegate(StaticHookMethod hook) - { - return (context, cancellationToken) => - { - return HookTimeoutHelper.CreateTimeoutHookAction( - hook, - context, - cancellationToken); - }; - } - - private async Task> CreateTestDiscoveryHookDelegateAsync(StaticHookMethod hook) - { - // Process hook registration event receivers - await ProcessHookRegistrationAsync(hook); - - return (context, cancellationToken) => - { - return HookTimeoutHelper.CreateTimeoutHookAction( - hook, - context, - cancellationToken); - }; + return new NamedHookDelegate(hook.Name, (context, cancellationToken) => + HookTimeoutHelper.CreateTimeoutHookAction(hook, context, cancellationToken)); } } diff --git a/TUnit.Engine/Services/HookExecutor.cs b/TUnit.Engine/Services/HookExecutor.cs index 6a24647154..47911cbe38 100644 --- a/TUnit.Engine/Services/HookExecutor.cs +++ b/TUnit.Engine/Services/HookExecutor.cs @@ -59,7 +59,7 @@ public async ValueTask ExecuteBeforeTestSessionHooksAsync(CancellationToken canc try { _contextProvider.TestSessionContext.RestoreExecutionContext(); - await hook(_contextProvider.TestSessionContext, cancellationToken).ConfigureAwait(false); + await ExecuteHookWithActivityAsync(hook, _contextProvider.TestSessionContext, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -98,7 +98,7 @@ public async ValueTask> ExecuteAfterTestSessionHooksAsync(Cancel try { _contextProvider.TestSessionContext.RestoreExecutionContext(); - await hook(_contextProvider.TestSessionContext, cancellationToken).ConfigureAwait(false); + await ExecuteHookWithActivityAsync(hook, _contextProvider.TestSessionContext, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -168,9 +168,8 @@ public async ValueTask ExecuteBeforeAssemblyHooksAsync(Assembly assembly, Cancel { try { - var context = _contextProvider.GetOrCreateAssemblyContext(assembly); - context.RestoreExecutionContext(); - await hook(context, cancellationToken).ConfigureAwait(false); + assemblyContext.RestoreExecutionContext(); + await ExecuteHookWithActivityAsync(hook, assemblyContext, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -191,6 +190,7 @@ public async ValueTask ExecuteBeforeAssemblyHooksAsync(Assembly assembly, Cancel public async ValueTask> ExecuteAfterAssemblyHooksAsync(Assembly assembly, CancellationToken cancellationToken) { + var afterAssemblyContext = _contextProvider.GetOrCreateAssemblyContext(assembly); var hooks = await _hookCollectionService.CollectAfterAssemblyHooksAsync(assembly).ConfigureAwait(false); if (hooks.Count == 0) @@ -208,9 +208,8 @@ public async ValueTask> ExecuteAfterAssemblyHooksAsync(Assembly { try { - var context = _contextProvider.GetOrCreateAssemblyContext(assembly); - context.RestoreExecutionContext(); - await hook(context, cancellationToken).ConfigureAwait(false); + afterAssemblyContext.RestoreExecutionContext(); + await ExecuteHookWithActivityAsync(hook, afterAssemblyContext, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -283,9 +282,8 @@ public async ValueTask ExecuteBeforeClassHooksAsync( { try { - var context = _contextProvider.GetOrCreateClassContext(testClass); - context.RestoreExecutionContext(); - await hook(context, cancellationToken).ConfigureAwait(false); + classContext.RestoreExecutionContext(); + await ExecuteHookWithActivityAsync(hook, classContext, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -308,6 +306,7 @@ public async ValueTask> ExecuteAfterClassHooksAsync( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)] Type testClass, CancellationToken cancellationToken) { + var afterClassContext = _contextProvider.GetOrCreateClassContext(testClass); var hooks = await _hookCollectionService.CollectAfterClassHooksAsync(testClass).ConfigureAwait(false); if (hooks.Count == 0) @@ -325,9 +324,8 @@ public async ValueTask> ExecuteAfterClassHooksAsync( { try { - var context = _contextProvider.GetOrCreateClassContext(testClass); - context.RestoreExecutionContext(); - await hook(context, cancellationToken).ConfigureAwait(false); + afterClassContext.RestoreExecutionContext(); + await ExecuteHookWithActivityAsync(hook, afterClassContext, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -384,7 +382,7 @@ public async ValueTask ExecuteBeforeTestHooksAsync(AbstractExecutableTest test, try { test.Context.RestoreExecutionContext(); - await hook(test.Context, cancellationToken).ConfigureAwait(false); + await ExecuteHookWithActivityAsync(hook, test.Context, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -413,7 +411,7 @@ public async ValueTask ExecuteBeforeTestHooksAsync(AbstractExecutableTest test, try { test.Context.RestoreExecutionContext(); - await hook(test.Context, cancellationToken).ConfigureAwait(false); + await ExecuteHookWithActivityAsync(hook, test.Context, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -449,7 +447,7 @@ public async ValueTask> ExecuteAfterTestHooksAsync(Abst try { test.Context.RestoreExecutionContext(); - await hook(test.Context, cancellationToken).ConfigureAwait(false); + await ExecuteHookWithActivityAsync(hook, test.Context, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -469,7 +467,7 @@ public async ValueTask> ExecuteAfterTestHooksAsync(Abst try { test.Context.RestoreExecutionContext(); - await hook(test.Context, cancellationToken).ConfigureAwait(false); + await ExecuteHookWithActivityAsync(hook, test.Context, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -479,7 +477,7 @@ public async ValueTask> ExecuteAfterTestHooksAsync(Abst } } - return exceptions == null ? Array.Empty() : exceptions; + return exceptions ?? []; } public async ValueTask ExecuteBeforeTestDiscoveryHooksAsync(CancellationToken cancellationToken) @@ -496,7 +494,7 @@ public async ValueTask ExecuteBeforeTestDiscoveryHooksAsync(CancellationToken ca try { _contextProvider.BeforeTestDiscoveryContext.RestoreExecutionContext(); - await hook(_contextProvider.BeforeTestDiscoveryContext, cancellationToken).ConfigureAwait(false); + await ExecuteHookWithActivityAsync(hook, _contextProvider.BeforeTestDiscoveryContext, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -519,7 +517,7 @@ public async ValueTask ExecuteAfterTestDiscoveryHooksAsync(CancellationToken can try { _contextProvider.TestDiscoveryContext.RestoreExecutionContext(); - await hook(_contextProvider.TestDiscoveryContext, cancellationToken).ConfigureAwait(false); + await ExecuteHookWithActivityAsync(hook, _contextProvider.TestDiscoveryContext, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -527,4 +525,40 @@ public async ValueTask ExecuteAfterTestDiscoveryHooksAsync(CancellationToken can } } } + +#if NET + private static async ValueTask ExecuteHookWithActivityAsync(NamedHookDelegate hook, TContext context, CancellationToken cancellationToken) + where TContext : Context + { + System.Diagnostics.Activity? hookActivity = null; + + if (TUnitActivitySource.Source.HasListeners()) + { + hookActivity = TUnitActivitySource.StartActivity( + hook.ActivityName, + System.Diagnostics.ActivityKind.Internal, + context.Activity?.Context ?? default); + } + + try + { + await hook.Invoke(context, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + TUnitActivitySource.RecordException(hookActivity, ex); + throw; + } + finally + { + TUnitActivitySource.StopActivity(hookActivity); + } + } +#else + private static async ValueTask ExecuteHookWithActivityAsync(NamedHookDelegate hook, TContext context, CancellationToken cancellationToken) + where TContext : Context + { + await hook.Invoke(context, cancellationToken).ConfigureAwait(false); + } +#endif } diff --git a/TUnit.Engine/TestExecutor.cs b/TUnit.Engine/TestExecutor.cs index ddd7139fd6..125ad8e676 100644 --- a/TUnit.Engine/TestExecutor.cs +++ b/TUnit.Engine/TestExecutor.cs @@ -147,36 +147,7 @@ await _eventReceiverOrchestrator.InvokeFirstTestInClassEventReceiversAsync( executableTest.Context.RestoreExecutionContext(); -#if NET - Activity? beforeTestActivity = null; - if (TUnitActivitySource.Source.HasListeners()) - { - beforeTestActivity = TUnitActivitySource.StartActivity( - "hook: BeforeTest", - ActivityKind.Internal, - executableTest.Context.Activity?.Context ?? default); - } -#endif - try - { - await _hookExecutor.ExecuteBeforeTestHooksAsync(executableTest, cancellationToken).ConfigureAwait(false); - } - catch -#if NET - (Exception ex) -#endif - { -#if NET - TUnitActivitySource.RecordException(beforeTestActivity, ex); -#endif - throw; - } - finally - { -#if NET - TUnitActivitySource.StopActivity(beforeTestActivity); -#endif - } + await _hookExecutor.ExecuteBeforeTestHooksAsync(executableTest, cancellationToken).ConfigureAwait(false); // Late stage test start receivers run after instance-level hooks (default behavior) await _eventReceiverOrchestrator.InvokeTestStartEventReceiversAsync(executableTest.Context, cancellationToken, EventReceiverStage.Late).ConfigureAwait(false); @@ -245,31 +216,7 @@ await TimeoutHelper.ExecuteWithTimeoutAsync( // Early stage test end receivers run before instance-level hooks var earlyStageExceptions = await _eventReceiverOrchestrator.InvokeTestEndEventReceiversAsync(executableTest.Context, CancellationToken.None, EventReceiverStage.Early).ConfigureAwait(false); - IReadOnlyList hookExceptions = []; -#if NET - Activity? afterTestActivity = null; - if (TUnitActivitySource.Source.HasListeners()) - { - afterTestActivity = TUnitActivitySource.StartActivity( - "hook: AfterTest", - ActivityKind.Internal, - executableTest.Context.Activity?.Context ?? default); - } -#endif - try - { - hookExceptions = await _hookExecutor.ExecuteAfterTestHooksAsync(executableTest, CancellationToken.None).ConfigureAwait(false); - } - finally - { -#if NET - if (hookExceptions.Count > 0 && afterTestActivity is not null) - { - afterTestActivity.SetStatus(ActivityStatusCode.Error); - } - TUnitActivitySource.StopActivity(afterTestActivity); -#endif - } + var hookExceptions = await _hookExecutor.ExecuteAfterTestHooksAsync(executableTest, CancellationToken.None).ConfigureAwait(false); // Late stage test end receivers run after instance-level hooks (default behavior) var lateStageExceptions = await _eventReceiverOrchestrator.InvokeTestEndEventReceiversAsync(executableTest.Context, CancellationToken.None, EventReceiverStage.Late).ConfigureAwait(false);