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 @@ -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}\")]");

Expand All @@ -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\");");
}
}

Expand Down
59 changes: 59 additions & 0 deletions TUnit.Core/Logging/EarlyBufferLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Collections.Concurrent;

namespace TUnit.Core.Logging;

/// <summary>
/// Logger that buffers messages until the real logger is configured.
/// Used as the initial GlobalLogger before TUnit infrastructure is set up.
/// </summary>
internal sealed class EarlyBufferLogger : ILogger
{
private readonly ConcurrentQueue<(LogLevel level, string message)> _buffer = new();

public bool IsEnabled(LogLevel logLevel) => true;

public ValueTask LogAsync<TState>(LogLevel logLevel, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
var message = formatter(state, exception);
_buffer.Enqueue((logLevel, message));
return ValueTask.CompletedTask;
}

public void Log<TState>(LogLevel logLevel, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
var message = formatter(state, exception);
_buffer.Enqueue((logLevel, message));
}

/// <summary>
/// Flushes all buffered messages to the provided logger.
/// </summary>
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;
}
}
}
}
17 changes: 16 additions & 1 deletion TUnit.Core/Models/GlobalContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions TUnit.Engine/Building/TestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -31,7 +31,7 @@ public TestBuilder(
EventReceiverOrchestrator eventReceiverOrchestrator,
IContextProvider contextProvider,
ObjectLifecycleService objectLifecycleService,
Discovery.IHookDiscoveryService hookDiscoveryService,
Discovery.IHookRegistrar hookDiscoveryService,
TestArgumentRegistrationService testArgumentRegistrationService,
IMetadataFilterMatcher filterMatcher)
{
Expand Down
17 changes: 0 additions & 17 deletions TUnit.Engine/Discovery/IHookDiscoveryService.cs

This file was deleted.

17 changes: 17 additions & 0 deletions TUnit.Engine/Discovery/IHookRegistrar.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace TUnit.Engine.Discovery;

/// <summary>
/// Registers hooks into Sources collections based on the execution mode (source generation or reflection).
/// </summary>
internal interface IHookRegistrar
{
/// <summary>
/// Discovers and registers all hooks for the test session into Sources collections.
/// </summary>
void DiscoverHooks();

/// <summary>
/// Discovers and registers instance hooks for a specific type (used for closed generic types).
/// </summary>
void DiscoverInstanceHooksForType(Type type);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
namespace TUnit.Engine.Discovery;

/// <summary>
/// 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.
/// </summary>
#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
{
/// <summary>
/// Discovers hooks using reflection by scanning all loaded assemblies.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
namespace TUnit.Engine.Discovery;

/// <summary>
/// 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.
/// </summary>
internal sealed class SourceGenHookDiscoveryService : IHookDiscoveryService
internal sealed class SourceGenHookRegistrar : IHookRegistrar
{
/// <summary>
/// No-op implementation. Hooks are already registered via source generation.
Expand Down
14 changes: 7 additions & 7 deletions TUnit.Engine/Framework/TUnitServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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<IHookDiscoveryService>(new SourceGenHookDiscoveryService());
hookDiscoveryService = Register<IHookRegistrar>(new SourceGenHookRegistrar());
}
else
{
hookDiscoveryService = Register<IHookDiscoveryService>(new ReflectionBasedHookDiscoveryService());
hookDiscoveryService = Register<IHookRegistrar>(new ReflectionHookRegistrar());
}

Initializer = new TUnitInitializer(CommandLineOptions, hookDiscoveryService);
Expand Down Expand Up @@ -161,13 +161,13 @@ public TUnitServiceProvider(IExtension extension,
CancellationToken = Register(new EngineCancellationToken());

EventReceiverOrchestrator = Register(new EventReceiverOrchestrator(Logger));
HookCollectionService = Register<IHookCollectionService>(new HookCollectionService(EventReceiverOrchestrator));
HookDelegateBuilder = Register<IHookDelegateBuilder>(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());
Expand Down
6 changes: 1 addition & 5 deletions TUnit.Engine/Framework/TUnitTestFramework.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Engine/Helpers/HookTimeoutHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public static Func<Task> CreateTimeoutHookAction<T>(
/// <summary>
/// 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
/// </summary>
public static Func<Task> CreateTimeoutHookAction<T>(
Func<T, CancellationToken, ValueTask> hookDelegate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@

namespace TUnit.Engine.Interfaces;

internal interface IHookCollectionService
/// <summary>
/// Builds executable hook delegates from Sources collections.
/// Responsible for converting hook metadata into Func delegates ready for execution.
/// </summary>
internal interface IHookDelegateBuilder
{
/// <summary>
/// Eagerly initializes all global hook delegates at startup.
/// </summary>
ValueTask InitializeAsync();

ValueTask<IReadOnlyList<Func<TestContext, CancellationToken, Task>>> CollectBeforeTestHooksAsync(Type testClassType);
ValueTask<IReadOnlyList<Func<TestContext, CancellationToken, Task>>> CollectAfterTestHooksAsync(Type testClassType);
ValueTask<IReadOnlyList<Func<TestContext, CancellationToken, Task>>> CollectBeforeEveryTestHooksAsync(Type testClassType);
Expand Down
Loading
Loading