From 216e523aca87085171cc6a8b00c82316299b327a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 5 Oct 2025 17:41:33 +0100 Subject: [PATCH] feat: add log level command line option for test output --- TUnit.Core/Logging/DefaultLogger.cs | 10 +++ .../LogLevelCommandProvider.cs | 79 +++++++++++++++++++ .../TestApplicationBuilderExtensions.cs | 1 + .../Framework/TUnitServiceProvider.cs | 5 +- TUnit.Engine/Logging/TUnitFrameworkLogger.cs | 5 +- TUnit.Engine/Services/LogLevelProvider.cs | 10 +-- docs/docs/reference/command-line-flags.md | 5 ++ 7 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 TUnit.Engine/CommandLineProviders/LogLevelCommandProvider.cs diff --git a/TUnit.Core/Logging/DefaultLogger.cs b/TUnit.Core/Logging/DefaultLogger.cs index 3874a16bac..8917a086ce 100644 --- a/TUnit.Core/Logging/DefaultLogger.cs +++ b/TUnit.Core/Logging/DefaultLogger.cs @@ -45,6 +45,11 @@ private static string FormatValue(object? value) public override async ValueTask LogAsync(LogLevel logLevel, TState state, Exception? exception, Func formatter) { + if (!IsEnabled(logLevel)) + { + return; + } + var message = GenerateMessage(formatter(state, exception), exception, logLevel); if (logLevel >= LogLevel.Error) @@ -61,6 +66,11 @@ public override async ValueTask LogAsync(LogLevel logLevel, TState state public override void Log(LogLevel logLevel, TState state, Exception? exception, Func formatter) { + if (!IsEnabled(logLevel)) + { + return; + } + var message = GenerateMessage(formatter(state, exception), exception, logLevel); if (logLevel >= LogLevel.Error) diff --git a/TUnit.Engine/CommandLineProviders/LogLevelCommandProvider.cs b/TUnit.Engine/CommandLineProviders/LogLevelCommandProvider.cs new file mode 100644 index 0000000000..ee75b713d0 --- /dev/null +++ b/TUnit.Engine/CommandLineProviders/LogLevelCommandProvider.cs @@ -0,0 +1,79 @@ +using Microsoft.Testing.Platform.CommandLine; +using Microsoft.Testing.Platform.Extensions; +using Microsoft.Testing.Platform.Extensions.CommandLine; +using LogLevel = TUnit.Core.Logging.LogLevel; + +namespace TUnit.Engine.CommandLineProviders; + +internal class LogLevelCommandProvider(IExtension extension) : ICommandLineOptionsProvider +{ + public const string LogLevelOption = "log-level"; + + public Task IsEnabledAsync() + { + return extension.IsEnabledAsync(); + } + + public string Uid => extension.Uid; + + public string Version => extension.Version; + + public string DisplayName => extension.DisplayName; + + public string Description => extension.Description; + + public IReadOnlyCollection GetCommandLineOptions() + { + return + [ + new CommandLineOption(LogLevelOption, "Minimum log level for test output: Trace, Debug, Information, Warning, Error, Critical, None (default: Information)", ArgumentArity.ExactlyOne, false) + ]; + } + + public Task ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments) + { + if (commandOption.Name == LogLevelOption && arguments.Length != 1) + { + return ValidationResult.InvalidTask("A single log level must be provided: Trace, Debug, Information, Warning, Error, Critical, or None"); + } + + if (commandOption.Name == LogLevelOption) + { + var logLevelArg = arguments[0]; + if (!IsValidLogLevel(logLevelArg)) + { + return ValidationResult.InvalidTask($"Invalid log level '{arguments[0]}'. Valid options: Trace, Debug, Information, Warning, Error, Critical, None"); + } + } + + return ValidationResult.ValidTask; + } + + public Task ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions) + { + return ValidationResult.ValidTask; + } + + private static bool IsValidLogLevel(string logLevel) + { + return Enum.TryParse(logLevel, ignoreCase: true, out _); + } + + /// + /// Parses log level from command line arguments + /// + public static LogLevel ParseLogLevel(string[] arguments) + { + if (arguments.Length == 0) + { + return LogLevel.Information; + } + + if (Enum.TryParse(arguments[0], ignoreCase: true, out var result)) + { + return result; + } + + return LogLevel.Information; + } +} diff --git a/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs b/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs index eb3313333c..cd1e569161 100644 --- a/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs +++ b/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs @@ -34,6 +34,7 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder) testApplicationBuilder.CommandLine.AddProvider(() => new FailFastCommandProvider(extension)); testApplicationBuilder.CommandLine.AddProvider(() => new ReflectionModeCommandProvider(extension)); testApplicationBuilder.CommandLine.AddProvider(() => new DisableLogoCommandProvider(extension)); + testApplicationBuilder.CommandLine.AddProvider(() => new LogLevelCommandProvider(extension)); // Adaptive parallelism command providers testApplicationBuilder.CommandLine.AddProvider(() => new ParallelismStrategyCommandProvider(extension)); diff --git a/TUnit.Engine/Framework/TUnitServiceProvider.cs b/TUnit.Engine/Framework/TUnitServiceProvider.cs index 23b0a2329d..6b19976aff 100644 --- a/TUnit.Engine/Framework/TUnitServiceProvider.cs +++ b/TUnit.Engine/Framework/TUnitServiceProvider.cs @@ -78,6 +78,8 @@ public TUnitServiceProvider(IExtension extension, VerbosityService = Register(new VerbosityService(CommandLineOptions)); DiscoveryDiagnostics.Initialize(VerbosityService); + var logLevelProvider = Register(new LogLevelProvider(CommandLineOptions)); + // Determine execution mode early to create appropriate services var useSourceGeneration = SourceRegistrar.IsEnabled = GetUseSourceGeneration(CommandLineOptions); @@ -98,7 +100,8 @@ public TUnitServiceProvider(IExtension extension, extension, outputDevice, loggerFactory.CreateLogger(), - VerbosityService)); + VerbosityService, + logLevelProvider)); // Create initialization services early as they're needed by other services DataSourceInitializer = Register(new DataSourceInitializer()); diff --git a/TUnit.Engine/Logging/TUnitFrameworkLogger.cs b/TUnit.Engine/Logging/TUnitFrameworkLogger.cs index 4653b30519..d29ccb510d 100644 --- a/TUnit.Engine/Logging/TUnitFrameworkLogger.cs +++ b/TUnit.Engine/Logging/TUnitFrameworkLogger.cs @@ -7,10 +7,11 @@ namespace TUnit.Engine.Logging; -public class TUnitFrameworkLogger(IExtension extension, IOutputDevice outputDevice, ILogger logger, VerbosityService verbosityService) +public class TUnitFrameworkLogger(IExtension extension, IOutputDevice outputDevice, ILogger logger, VerbosityService verbosityService, LogLevelProvider logLevelProvider) : IOutputDeviceDataProducer, Core.Logging.ILogger { private readonly bool _hideTestOutput = verbosityService.HideTestOutput; + private readonly LogLevel _minimumLogLevel = logLevelProvider.LogLevel; private readonly MTPLoggerAdapter _adapter = new(logger); @@ -90,7 +91,7 @@ private static ConsoleColor GetConsoleColor(LogLevel logLevel) public bool IsEnabled(LogLevel logLevel) { - return !_hideTestOutput && logger.IsEnabled(MTPLoggerAdapter.Map(logLevel)); + return !_hideTestOutput && logLevel >= _minimumLogLevel && logger.IsEnabled(MTPLoggerAdapter.Map(logLevel)); } public async Task LogErrorAsync(string message) diff --git a/TUnit.Engine/Services/LogLevelProvider.cs b/TUnit.Engine/Services/LogLevelProvider.cs index 7a22f210dc..fd13eeae5a 100644 --- a/TUnit.Engine/Services/LogLevelProvider.cs +++ b/TUnit.Engine/Services/LogLevelProvider.cs @@ -1,19 +1,19 @@ using Microsoft.Testing.Platform.CommandLine; -using TUnit.Core.Enums; +using TUnit.Core.Logging; +using TUnit.Engine.CommandLineProviders; namespace TUnit.Engine.Services; -internal class LogLevelProvider(ICommandLineOptions commandLineOptions) +public class LogLevelProvider(ICommandLineOptions commandLineOptions) { internal static LogLevel? _logLevel; public LogLevel LogLevel => _logLevel ??= GetLogLevel(); private LogLevel GetLogLevel() { - if (commandLineOptions.TryGetOptionArgumentList("log-level", out var values) - && Enum.TryParse(values.FirstOrDefault(), out var parsedResult)) + if (commandLineOptions.TryGetOptionArgumentList(LogLevelCommandProvider.LogLevelOption, out var values)) { - return parsedResult; + return LogLevelCommandProvider.ParseLogLevel(values); } return LogLevel.Information; diff --git a/docs/docs/reference/command-line-flags.md b/docs/docs/reference/command-line-flags.md index fa16c6c450..a7a3dcca2c 100644 --- a/docs/docs/reference/command-line-flags.md +++ b/docs/docs/reference/command-line-flags.md @@ -43,6 +43,11 @@ Please note that for the coverage and trx report, you need to install [additiona --list-tests List available tests. + --log-level + Minimum log level for test output. + The available values are 'Trace', 'Debug', 'Information', 'Warning', 'Error', 'Critical', and 'None'. + Default is 'Information'. + --minimum-expected-tests Specifies the minimum number of tests that are expected to run.