diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/InfrastructureGenerator.cs b/TUnit.Core.SourceGenerator/CodeGenerators/InfrastructureGenerator.cs index 2836281261..bb1767cc61 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/InfrastructureGenerator.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/InfrastructureGenerator.cs @@ -305,6 +305,10 @@ private static void GenerateCode(SourceProductionContext context, AssemblyInfoMo { var sourceBuilder = new CodeWriter(); + // Add using directive for LogDebug extension method + sourceBuilder.AppendLine("using TUnit.Core.Logging;"); + sourceBuilder.AppendLine(); + sourceBuilder.AppendLine("[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute]"); sourceBuilder.AppendLine($"[global::System.CodeDom.Compiler.GeneratedCode(\"TUnit\", \"{typeof(InfrastructureGenerator).Assembly.GetName().Version}\")]"); @@ -314,25 +318,40 @@ private static void GenerateCode(SourceProductionContext context, AssemblyInfoMo sourceBuilder.AppendLine("[global::System.Runtime.CompilerServices.ModuleInitializer]"); using (sourceBuilder.BeginBlock("public static void Initialize()")) { + // Log module initializer start (buffered until logger is ready) + sourceBuilder.AppendLine("global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogDebug(\"[ModuleInitializer] TUnit infrastructure initializing...\");"); + sourceBuilder.AppendLine(); + // Disable reflection scanner for source-generated assemblies sourceBuilder.AppendLine("global::TUnit.Core.SourceRegistrar.IsEnabled = true;"); + sourceBuilder.AppendLine("global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogDebug(\"[ModuleInitializer] Source generation mode enabled\");"); + sourceBuilder.AppendLine(); // Reference types from assemblies to trigger their module constructors + if (model.TypesToReference.Length > 0) + { + sourceBuilder.AppendLine($"global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogDebug(\"[ModuleInitializer] Loading {model.TypesToReference.Length} assembly reference(s)...\");"); + } + foreach (var typeName in model.TypesToReference) { sourceBuilder.AppendLine("try"); sourceBuilder.AppendLine("{"); sourceBuilder.Indent(); + sourceBuilder.AppendLine($"global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogDebug(\"[ModuleInitializer] Loading assembly containing: {typeName.Replace("\"", "\\\"")}\");"); sourceBuilder.AppendLine($"_ = typeof({typeName});"); sourceBuilder.Unindent(); sourceBuilder.AppendLine("}"); - sourceBuilder.AppendLine("catch (global::System.Exception)"); + sourceBuilder.AppendLine("catch (global::System.Exception ex)"); sourceBuilder.AppendLine("{"); sourceBuilder.Indent(); - sourceBuilder.AppendLine("// Type reference failed - continue"); + sourceBuilder.AppendLine($"global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogDebug(\"[ModuleInitializer] Failed to load {typeName.Replace("\"", "\\\"")}: \" + ex.Message);"); sourceBuilder.Unindent(); sourceBuilder.AppendLine("}"); } + + sourceBuilder.AppendLine(); + sourceBuilder.AppendLine("global::TUnit.Core.GlobalContext.Current.GlobalLogger.LogDebug(\"[ModuleInitializer] TUnit infrastructure initialized\");"); } } diff --git a/TUnit.Core/Logging/EarlyBufferLogger.cs b/TUnit.Core/Logging/EarlyBufferLogger.cs new file mode 100644 index 0000000000..bf1a0c31ce --- /dev/null +++ b/TUnit.Core/Logging/EarlyBufferLogger.cs @@ -0,0 +1,59 @@ +using System.Collections.Concurrent; + +namespace TUnit.Core.Logging; + +/// +/// Logger that buffers messages until the real logger is configured. +/// Used as the initial GlobalLogger before TUnit infrastructure is set up. +/// +internal sealed class EarlyBufferLogger : ILogger +{ + private readonly ConcurrentQueue<(LogLevel level, string message)> _buffer = new(); + + public bool IsEnabled(LogLevel logLevel) => true; + + public ValueTask LogAsync(LogLevel logLevel, TState state, Exception? exception, Func formatter) + { + var message = formatter(state, exception); + _buffer.Enqueue((logLevel, message)); + return ValueTask.CompletedTask; + } + + public void Log(LogLevel logLevel, TState state, Exception? exception, Func formatter) + { + var message = formatter(state, exception); + _buffer.Enqueue((logLevel, message)); + } + + /// + /// Flushes all buffered messages to the provided logger. + /// + internal void FlushTo(ILogger logger) + { + while (_buffer.TryDequeue(out var entry)) + { + var (level, message) = entry; + switch (level) + { + case LogLevel.Trace: + logger.LogTrace(message); + break; + case LogLevel.Debug: + logger.LogDebug(message); + break; + case LogLevel.Information: + logger.LogInformation(message); + break; + case LogLevel.Warning: + logger.LogWarning(message); + break; + case LogLevel.Error: + logger.LogError(message); + break; + case LogLevel.Critical: + logger.LogCritical(message); + break; + } + } + } +} diff --git a/TUnit.Core/Models/GlobalContext.cs b/TUnit.Core/Models/GlobalContext.cs index 57834c4ae2..fa88dd432f 100644 --- a/TUnit.Core/Models/GlobalContext.cs +++ b/TUnit.Core/Models/GlobalContext.cs @@ -20,7 +20,22 @@ internal GlobalContext() : base(null) { } - internal ILogger GlobalLogger { get; set; } = new NullLogger(); + private ILogger _globalLogger = new Logging.EarlyBufferLogger(); + + public ILogger GlobalLogger + { + get => _globalLogger; + internal set + { + // Flush buffered logs to the new logger + if (_globalLogger is Logging.EarlyBufferLogger bufferLogger) + { + bufferLogger.FlushTo(value); + } + + _globalLogger = value; + } + } public string? TestFilter { get; internal set; } public TextWriter OriginalConsoleOut { get; set; } = Console.Out; diff --git a/TUnit.Engine/Building/TestBuilder.cs b/TUnit.Engine/Building/TestBuilder.cs index 341439352d..3d3c1ad23c 100644 --- a/TUnit.Engine/Building/TestBuilder.cs +++ b/TUnit.Engine/Building/TestBuilder.cs @@ -22,7 +22,7 @@ internal sealed class TestBuilder : ITestBuilder private readonly EventReceiverOrchestrator _eventReceiverOrchestrator; private readonly IContextProvider _contextProvider; private readonly ObjectLifecycleService _objectLifecycleService; - private readonly Discovery.IHookDiscoveryService _hookDiscoveryService; + private readonly Discovery.IHookRegistrar _hookDiscoveryService; private readonly TestArgumentRegistrationService _testArgumentRegistrationService; private readonly IMetadataFilterMatcher _filterMatcher; @@ -31,7 +31,7 @@ public TestBuilder( EventReceiverOrchestrator eventReceiverOrchestrator, IContextProvider contextProvider, ObjectLifecycleService objectLifecycleService, - Discovery.IHookDiscoveryService hookDiscoveryService, + Discovery.IHookRegistrar hookDiscoveryService, TestArgumentRegistrationService testArgumentRegistrationService, IMetadataFilterMatcher filterMatcher) { diff --git a/TUnit.Engine/Discovery/IHookDiscoveryService.cs b/TUnit.Engine/Discovery/IHookDiscoveryService.cs deleted file mode 100644 index ac392694a3..0000000000 --- a/TUnit.Engine/Discovery/IHookDiscoveryService.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace TUnit.Engine.Discovery; - -/// -/// Discovers hooks based on the execution mode (source generation or reflection). -/// -internal interface IHookDiscoveryService -{ - /// - /// Discovers and registers all hooks for the test session. - /// - void DiscoverHooks(); - - /// - /// Discovers instance hooks for a specific type (used for closed generic types). - /// - void DiscoverInstanceHooksForType(Type type); -} diff --git a/TUnit.Engine/Discovery/IHookRegistrar.cs b/TUnit.Engine/Discovery/IHookRegistrar.cs new file mode 100644 index 0000000000..6ab34c2efb --- /dev/null +++ b/TUnit.Engine/Discovery/IHookRegistrar.cs @@ -0,0 +1,17 @@ +namespace TUnit.Engine.Discovery; + +/// +/// Registers hooks into Sources collections based on the execution mode (source generation or reflection). +/// +internal interface IHookRegistrar +{ + /// + /// Discovers and registers all hooks for the test session into Sources collections. + /// + void DiscoverHooks(); + + /// + /// Discovers and registers instance hooks for a specific type (used for closed generic types). + /// + void DiscoverInstanceHooksForType(Type type); +} diff --git a/TUnit.Engine/Discovery/ReflectionBasedHookDiscoveryService.cs b/TUnit.Engine/Discovery/ReflectionHookRegistrar.cs similarity index 70% rename from TUnit.Engine/Discovery/ReflectionBasedHookDiscoveryService.cs rename to TUnit.Engine/Discovery/ReflectionHookRegistrar.cs index 2adba1e3d9..38ba2f8ebf 100644 --- a/TUnit.Engine/Discovery/ReflectionBasedHookDiscoveryService.cs +++ b/TUnit.Engine/Discovery/ReflectionHookRegistrar.cs @@ -3,14 +3,14 @@ namespace TUnit.Engine.Discovery; /// -/// Hook discovery service for reflection mode. -/// Uses reflection to scan assemblies and discover hooks at runtime. +/// Hook registrar for reflection mode. +/// Uses reflection to scan assemblies and register hooks into Sources at runtime. /// This implementation requires reflection and is NOT AOT-compatible. /// #if NET6_0_OR_GREATER -[RequiresUnreferencedCode("Hook discovery uses reflection to scan assemblies and types")] +[RequiresUnreferencedCode("Hook registration uses reflection to scan assemblies and types")] #endif -internal sealed class ReflectionBasedHookDiscoveryService : IHookDiscoveryService +internal sealed class ReflectionHookRegistrar : IHookRegistrar { /// /// Discovers hooks using reflection by scanning all loaded assemblies. diff --git a/TUnit.Engine/Discovery/SourceGenHookDiscoveryService.cs b/TUnit.Engine/Discovery/SourceGenHookRegistrar.cs similarity index 76% rename from TUnit.Engine/Discovery/SourceGenHookDiscoveryService.cs rename to TUnit.Engine/Discovery/SourceGenHookRegistrar.cs index e237c8a5bf..26e37fc0d3 100644 --- a/TUnit.Engine/Discovery/SourceGenHookDiscoveryService.cs +++ b/TUnit.Engine/Discovery/SourceGenHookRegistrar.cs @@ -1,11 +1,11 @@ namespace TUnit.Engine.Discovery; /// -/// Hook discovery service for source generation mode. -/// In this mode, hooks are discovered at compile time via source generators, so no runtime discovery is needed. +/// Hook registrar for source generation mode. +/// In this mode, hooks are registered at compile time via source generators, so no runtime registration is needed. /// This implementation is AOT-compatible and does not use reflection. /// -internal sealed class SourceGenHookDiscoveryService : IHookDiscoveryService +internal sealed class SourceGenHookRegistrar : IHookRegistrar { /// /// No-op implementation. Hooks are already registered via source generation. diff --git a/TUnit.Engine/Framework/TUnitServiceProvider.cs b/TUnit.Engine/Framework/TUnitServiceProvider.cs index 6e26194ba1..7eb019614d 100644 --- a/TUnit.Engine/Framework/TUnitServiceProvider.cs +++ b/TUnit.Engine/Framework/TUnitServiceProvider.cs @@ -48,7 +48,7 @@ public ITestExecutionFilter? Filter public TUnitMessageBus MessageBus { get; } public EngineCancellationToken CancellationToken { get; } public TestFilterService TestFilterService { get; } - public IHookCollectionService HookCollectionService { get; } + public IHookDelegateBuilder HookDelegateBuilder { get; } public TestExecutor TestExecutor { get; } public EventReceiverOrchestrator EventReceiverOrchestrator { get; } public ITestFinder TestFinder { get; } @@ -87,15 +87,15 @@ public TUnitServiceProvider(IExtension extension, // Determine execution mode early to create appropriate services var useSourceGeneration = SourceRegistrar.IsEnabled = ExecutionModeHelper.IsSourceGenerationMode(CommandLineOptions); - // Create and register mode-specific hook discovery service - IHookDiscoveryService hookDiscoveryService; + // Create and register mode-specific hook registrar service + IHookRegistrar hookDiscoveryService; if (useSourceGeneration) { - hookDiscoveryService = Register(new SourceGenHookDiscoveryService()); + hookDiscoveryService = Register(new SourceGenHookRegistrar()); } else { - hookDiscoveryService = Register(new ReflectionBasedHookDiscoveryService()); + hookDiscoveryService = Register(new ReflectionHookRegistrar()); } Initializer = new TUnitInitializer(CommandLineOptions, hookDiscoveryService); @@ -161,13 +161,13 @@ public TUnitServiceProvider(IExtension extension, CancellationToken = Register(new EngineCancellationToken()); EventReceiverOrchestrator = Register(new EventReceiverOrchestrator(Logger)); - HookCollectionService = Register(new HookCollectionService(EventReceiverOrchestrator)); + HookDelegateBuilder = Register(new HookDelegateBuilder(EventReceiverOrchestrator, Logger)); ParallelLimitLockProvider = Register(new ParallelLimitLockProvider()); ContextProvider = Register(new ContextProvider(this, TestSessionId, Filter?.ToString())); - var hookExecutor = Register(new HookExecutor(HookCollectionService, ContextProvider, EventReceiverOrchestrator)); + var hookExecutor = Register(new HookExecutor(HookDelegateBuilder, ContextProvider, EventReceiverOrchestrator)); var lifecycleCoordinator = Register(new TestLifecycleCoordinator()); var beforeHookTaskCache = Register(new BeforeHookTaskCache()); var afterHookPairTracker = Register(new AfterHookPairTracker()); diff --git a/TUnit.Engine/Framework/TUnitTestFramework.cs b/TUnit.Engine/Framework/TUnitTestFramework.cs index 9c68fcc49c..f2a334056a 100644 --- a/TUnit.Engine/Framework/TUnitTestFramework.cs +++ b/TUnit.Engine/Framework/TUnitTestFramework.cs @@ -55,11 +55,7 @@ public async Task ExecuteRequestAsync(ExecuteRequestContext context) serviceProvider.Initializer.Initialize(context); - // Initialize hook collection service to pre-compute global hooks - if (serviceProvider.HookCollectionService is HookCollectionService hookCollectionService) - { - await hookCollectionService.InitializeAsync(); - } + await serviceProvider.HookDelegateBuilder.InitializeAsync(); GlobalContext.Current = serviceProvider.ContextProvider.GlobalContext; GlobalContext.Current.GlobalLogger = serviceProvider.Logger; diff --git a/TUnit.Engine/Helpers/HookTimeoutHelper.cs b/TUnit.Engine/Helpers/HookTimeoutHelper.cs index d252c37f2e..aa9b7ec074 100644 --- a/TUnit.Engine/Helpers/HookTimeoutHelper.cs +++ b/TUnit.Engine/Helpers/HookTimeoutHelper.cs @@ -131,7 +131,7 @@ public static Func CreateTimeoutHookAction( /// /// Creates a timeout-aware action wrapper for a hook delegate that returns ValueTask /// This overload is used for instance hooks (InstanceHookMethod) - /// Custom executor handling for instance hooks is done in HookCollectionService.CreateInstanceHookDelegateAsync + /// Custom executor handling for instance hooks is done in HookDelegateBuilder.CreateInstanceHookDelegateAsync /// public static Func CreateTimeoutHookAction( Func hookDelegate, diff --git a/TUnit.Engine/Interfaces/IHookCollectionService.cs b/TUnit.Engine/Interfaces/IHookDelegateBuilder.cs similarity index 85% rename from TUnit.Engine/Interfaces/IHookCollectionService.cs rename to TUnit.Engine/Interfaces/IHookDelegateBuilder.cs index 3b9d4a96f8..13354c314f 100644 --- a/TUnit.Engine/Interfaces/IHookCollectionService.cs +++ b/TUnit.Engine/Interfaces/IHookDelegateBuilder.cs @@ -3,8 +3,17 @@ namespace TUnit.Engine.Interfaces; -internal interface IHookCollectionService +/// +/// Builds executable hook delegates from Sources collections. +/// Responsible for converting hook metadata into Func delegates ready for execution. +/// +internal interface IHookDelegateBuilder { + /// + /// Eagerly initializes all global hook delegates at startup. + /// + ValueTask InitializeAsync(); + ValueTask>> CollectBeforeTestHooksAsync(Type testClassType); ValueTask>> CollectAfterTestHooksAsync(Type testClassType); ValueTask>> CollectBeforeEveryTestHooksAsync(Type testClassType); diff --git a/TUnit.Engine/Services/HookCollectionService.cs b/TUnit.Engine/Services/HookDelegateBuilder.cs similarity index 79% rename from TUnit.Engine/Services/HookCollectionService.cs rename to TUnit.Engine/Services/HookDelegateBuilder.cs index 0b9dd8b820..ab5eeb4439 100644 --- a/TUnit.Engine/Services/HookCollectionService.cs +++ b/TUnit.Engine/Services/HookDelegateBuilder.cs @@ -2,14 +2,21 @@ using System.Reflection; using TUnit.Core; using TUnit.Core.Hooks; +using TUnit.Core.Logging; using TUnit.Engine.Helpers; using TUnit.Engine.Interfaces; +using TUnit.Engine.Logging; namespace TUnit.Engine.Services; -internal sealed class HookCollectionService : IHookCollectionService +/// +/// Builds executable hook delegates from Sources collections with caching. +/// Reads hook metadata from Sources and compiles them into ready-to-execute Func delegates. +/// +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(); @@ -35,9 +42,10 @@ internal sealed class HookCollectionService : IHookCollectionService // Cache for processed hooks to avoid re-processing event receivers private readonly ConcurrentDictionary _processedHooks = new(); - public HookCollectionService(EventReceiverOrchestrator eventReceiverOrchestrator) + public HookDelegateBuilder(EventReceiverOrchestrator eventReceiverOrchestrator, TUnitFrameworkLogger logger) { _eventReceiverOrchestrator = eventReceiverOrchestrator; + _logger = logger; } private static Type GetCachedGenericTypeDefinition(Type type) @@ -47,6 +55,8 @@ private static Type GetCachedGenericTypeDefinition(Type type) public async ValueTask InitializeAsync() { + await _logger.LogDebugAsync("Building global hook delegates...").ConfigureAwait(false); + // Pre-compute all global hooks that don't depend on specific types/assemblies _beforeEveryTestHooks = await BuildGlobalBeforeEveryTestHooksAsync(); _afterEveryTestHooks = await BuildGlobalAfterEveryTestHooksAsync(); @@ -58,177 +68,78 @@ public async ValueTask InitializeAsync() _afterEveryClassHooks = await BuildGlobalAfterEveryClassHooksAsync(); _beforeEveryAssemblyHooks = await BuildGlobalBeforeEveryAssemblyHooksAsync(); _afterEveryAssemblyHooks = await BuildGlobalAfterEveryAssemblyHooksAsync(); + + var totalHooks = _beforeEveryTestHooks.Count + _afterEveryTestHooks.Count + + _beforeTestSessionHooks.Count + _afterTestSessionHooks.Count + + _beforeTestDiscoveryHooks.Count + _afterTestDiscoveryHooks.Count + + _beforeEveryClassHooks.Count + _afterEveryClassHooks.Count + + _beforeEveryAssemblyHooks.Count + _afterEveryAssemblyHooks.Count; + + await _logger.LogDebugAsync($"Built {totalHooks} global hook delegates").ConfigureAwait(false); } - private async Task>> BuildGlobalBeforeEveryTestHooksAsync() + /// + /// Generic helper to build global hooks from Sources collections. + /// Eliminates duplication across all BuildGlobalXXXHooksAsync methods. + /// + private async Task>> BuildGlobalHooksAsync( + IEnumerable sourceHooks, + Func>> createDelegate, + string hookTypeName) + where THookMethod : HookMethod { - var allHooks = new List<(int order, int registrationIndex, Func hook)>(Sources.BeforeEveryTestHooks.Count); + // 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); - foreach (var hook in Sources.BeforeEveryTestHooks) + foreach (var hook in sourceHooks) { - var hookFunc = await CreateStaticHookDelegateAsync(hook); - allHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); + await _logger.LogDebugAsync($"Creating delegate for {hookTypeName} hook: {hook.Name}").ConfigureAwait(false); + var hookFunc = await createDelegate(hook); + hooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); } - return allHooks - .OrderBy(static h => h.order) - .ThenBy(static h => h.registrationIndex) - .Select(static h => h.hook) - .ToList(); - } - - private async Task>> BuildGlobalAfterEveryTestHooksAsync() - { - var allHooks = new List<(int order, int registrationIndex, Func hook)>(Sources.AfterEveryTestHooks.Count); - - foreach (var hook in Sources.AfterEveryTestHooks) + if (hooks.Count > 0) { - var hookFunc = await CreateStaticHookDelegateAsync(hook); - allHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); + await _logger.LogDebugAsync($"Built {hooks.Count} {hookTypeName} hook delegate(s)").ConfigureAwait(false); } - return allHooks + return hooks .OrderBy(static h => h.order) .ThenBy(static h => h.registrationIndex) .Select(static h => h.hook) .ToList(); } - private async Task>> BuildGlobalBeforeTestSessionHooksAsync() - { - var allHooks = new List<(int order, int registrationIndex, Func hook)>(Sources.BeforeTestSessionHooks.Count); + private Task>> BuildGlobalBeforeEveryTestHooksAsync() + => BuildGlobalHooksAsync(Sources.BeforeEveryTestHooks, CreateStaticHookDelegateAsync, "BeforeEveryTest"); - foreach (var hook in Sources.BeforeTestSessionHooks) - { - var hookFunc = await CreateTestSessionHookDelegateAsync(hook); - allHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); - } + private Task>> BuildGlobalAfterEveryTestHooksAsync() + => BuildGlobalHooksAsync(Sources.AfterEveryTestHooks, CreateStaticHookDelegateAsync, "AfterEveryTest"); - return allHooks - .OrderBy(h => h.order) - .ThenBy(h => h.registrationIndex) - .Select(h => h.hook) - .ToList(); - } + private Task>> BuildGlobalBeforeTestSessionHooksAsync() + => BuildGlobalHooksAsync(Sources.BeforeTestSessionHooks, CreateTestSessionHookDelegateAsync, "BeforeTestSession"); - private async Task>> BuildGlobalAfterTestSessionHooksAsync() - { - var allHooks = new List<(int order, int registrationIndex, Func hook)>(Sources.AfterTestSessionHooks.Count); + private Task>> BuildGlobalAfterTestSessionHooksAsync() + => BuildGlobalHooksAsync(Sources.AfterTestSessionHooks, CreateTestSessionHookDelegateAsync, "AfterTestSession"); - foreach (var hook in Sources.AfterTestSessionHooks) - { - var hookFunc = await CreateTestSessionHookDelegateAsync(hook); - allHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); - } + private Task>> BuildGlobalBeforeTestDiscoveryHooksAsync() + => BuildGlobalHooksAsync(Sources.BeforeTestDiscoveryHooks, CreateBeforeTestDiscoveryHookDelegateAsync, "BeforeTestDiscovery"); - return allHooks - .OrderBy(h => h.order) - .ThenBy(h => h.registrationIndex) - .Select(h => h.hook) - .ToList(); - } + private Task>> BuildGlobalAfterTestDiscoveryHooksAsync() + => BuildGlobalHooksAsync(Sources.AfterTestDiscoveryHooks, CreateTestDiscoveryHookDelegateAsync, "AfterTestDiscovery"); - private async Task>> BuildGlobalBeforeTestDiscoveryHooksAsync() - { - var allHooks = new List<(int order, int registrationIndex, Func hook)>(Sources.BeforeTestDiscoveryHooks.Count); + private Task>> BuildGlobalBeforeEveryClassHooksAsync() + => BuildGlobalHooksAsync(Sources.BeforeEveryClassHooks, CreateClassHookDelegateAsync, "BeforeEveryClass"); - foreach (var hook in Sources.BeforeTestDiscoveryHooks) - { - var hookFunc = await CreateBeforeTestDiscoveryHookDelegateAsync(hook); - allHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); - } + private Task>> BuildGlobalAfterEveryClassHooksAsync() + => BuildGlobalHooksAsync(Sources.AfterEveryClassHooks, CreateClassHookDelegateAsync, "AfterEveryClass"); - return allHooks - .OrderBy(h => h.order) - .ThenBy(h => h.registrationIndex) - .Select(h => h.hook) - .ToList(); - } - - private async Task>> BuildGlobalAfterTestDiscoveryHooksAsync() - { - var allHooks = new List<(int order, int registrationIndex, Func hook)>(Sources.AfterTestDiscoveryHooks.Count); - - foreach (var hook in Sources.AfterTestDiscoveryHooks) - { - var hookFunc = await CreateTestDiscoveryHookDelegateAsync(hook); - allHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); - } + private Task>> BuildGlobalBeforeEveryAssemblyHooksAsync() + => BuildGlobalHooksAsync(Sources.BeforeEveryAssemblyHooks, CreateAssemblyHookDelegateAsync, "BeforeEveryAssembly"); - return allHooks - .OrderBy(h => h.order) - .ThenBy(h => h.registrationIndex) - .Select(h => h.hook) - .ToList(); - } - - private async Task>> BuildGlobalBeforeEveryClassHooksAsync() - { - var allHooks = new List<(int order, int registrationIndex, Func hook)>(Sources.BeforeEveryClassHooks.Count); - - foreach (var hook in Sources.BeforeEveryClassHooks) - { - var hookFunc = await CreateClassHookDelegateAsync(hook); - allHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); - } - - return allHooks - .OrderBy(h => h.order) - .ThenBy(h => h.registrationIndex) - .Select(h => h.hook) - .ToList(); - } - - private async Task>> BuildGlobalAfterEveryClassHooksAsync() - { - var allHooks = new List<(int order, int registrationIndex, Func hook)>(Sources.AfterEveryClassHooks.Count); - - foreach (var hook in Sources.AfterEveryClassHooks) - { - var hookFunc = await CreateClassHookDelegateAsync(hook); - allHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); - } - - return allHooks - .OrderBy(h => h.order) - .ThenBy(h => h.registrationIndex) - .Select(h => h.hook) - .ToList(); - } - - private async Task>> BuildGlobalBeforeEveryAssemblyHooksAsync() - { - var allHooks = new List<(int order, int registrationIndex, Func hook)>(Sources.BeforeEveryAssemblyHooks.Count); - - foreach (var hook in Sources.BeforeEveryAssemblyHooks) - { - var hookFunc = await CreateAssemblyHookDelegateAsync(hook); - allHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); - } - - return allHooks - .OrderBy(h => h.order) - .ThenBy(h => h.registrationIndex) - .Select(h => h.hook) - .ToList(); - } - - private async Task>> BuildGlobalAfterEveryAssemblyHooksAsync() - { - var allHooks = new List<(int order, int registrationIndex, Func hook)>(Sources.AfterEveryAssemblyHooks.Count); - - foreach (var hook in Sources.AfterEveryAssemblyHooks) - { - var hookFunc = await CreateAssemblyHookDelegateAsync(hook); - allHooks.Add((hook.Order, hook.RegistrationIndex, hookFunc)); - } - - return allHooks - .OrderBy(h => h.order) - .ThenBy(h => h.registrationIndex) - .Select(h => h.hook) - .ToList(); - } + private Task>> BuildGlobalAfterEveryAssemblyHooksAsync() + => BuildGlobalHooksAsync(Sources.AfterEveryAssemblyHooks, CreateAssemblyHookDelegateAsync, "AfterEveryAssembly"); private static void SortAndAddHooks( List> target, diff --git a/TUnit.Engine/Services/HookExecutor.cs b/TUnit.Engine/Services/HookExecutor.cs index 7f31691970..1fb083b56f 100644 --- a/TUnit.Engine/Services/HookExecutor.cs +++ b/TUnit.Engine/Services/HookExecutor.cs @@ -15,12 +15,12 @@ namespace TUnit.Engine.Services; /// internal sealed class HookExecutor { - private readonly IHookCollectionService _hookCollectionService; + private readonly IHookDelegateBuilder _hookCollectionService; private readonly IContextProvider _contextProvider; private readonly EventReceiverOrchestrator _eventReceiverOrchestrator; public HookExecutor( - IHookCollectionService hookCollectionService, + IHookDelegateBuilder hookCollectionService, IContextProvider contextProvider, EventReceiverOrchestrator eventReceiverOrchestrator) { diff --git a/TUnit.Engine/TUnitInitializer.cs b/TUnit.Engine/TUnitInitializer.cs index 3eb47fb89b..0e42f58d4a 100644 --- a/TUnit.Engine/TUnitInitializer.cs +++ b/TUnit.Engine/TUnitInitializer.cs @@ -8,7 +8,7 @@ namespace TUnit.Engine; -internal class TUnitInitializer(ICommandLineOptions commandLineOptions, IHookDiscoveryService hookDiscoveryService) +internal class TUnitInitializer(ICommandLineOptions commandLineOptions, IHookRegistrar hookDiscoveryService) { public void Initialize(ExecuteRequestContext context) { 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 207727b38c..0cc53f2a25 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 @@ -813,6 +813,7 @@ namespace } public class GlobalContext : .Context { + public . GlobalLogger { get; } public .TextWriter OriginalConsoleError { get; set; } public .TextWriter OriginalConsoleOut { get; set; } public string? TestFilter { get; } 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 ce82e785d5..1c555a47d5 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 @@ -813,6 +813,7 @@ namespace } public class GlobalContext : .Context { + public . GlobalLogger { get; } public .TextWriter OriginalConsoleError { get; set; } public .TextWriter OriginalConsoleOut { get; set; } public string? TestFilter { get; } 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 4e79f131df..de1cb1bc62 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 @@ -813,6 +813,7 @@ namespace } public class GlobalContext : .Context { + public . GlobalLogger { get; } public .TextWriter OriginalConsoleError { get; set; } public .TextWriter OriginalConsoleOut { get; set; } public string? TestFilter { get; } 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 a90159d043..319cb11fed 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 @@ -789,6 +789,7 @@ namespace } public class GlobalContext : .Context { + public . GlobalLogger { get; } public .TextWriter OriginalConsoleError { get; set; } public .TextWriter OriginalConsoleOut { get; set; } public string? TestFilter { get; }