diff --git a/TUnit.Core/Logging/DefaultLogger.cs b/TUnit.Core/Logging/DefaultLogger.cs index 76fc7c18e1..9479873ec2 100644 --- a/TUnit.Core/Logging/DefaultLogger.cs +++ b/TUnit.Core/Logging/DefaultLogger.cs @@ -7,6 +7,11 @@ public class DefaultLogger(Context context) : TUnitLogger { private readonly ConcurrentDictionary> _values = new(); + /// + /// Gets the context associated with this logger. + /// + protected Context Context => context; + public void PushProperties(IDictionary> dictionary) { foreach (var keyValuePair in dictionary) @@ -52,16 +57,7 @@ public override async ValueTask LogAsync(LogLevel logLevel, TState state var message = GenerateMessage(formatter(state, exception), exception, logLevel); - if (logLevel >= LogLevel.Error) - { - await context.ErrorOutputWriter.WriteLineAsync(message); - await GlobalContext.Current.OriginalConsoleError.WriteLineAsync(message); - } - else - { - await context.OutputWriter.WriteLineAsync(message); - await GlobalContext.Current.OriginalConsoleOut.WriteLineAsync(message); - } + await WriteToOutputAsync(message, logLevel >= LogLevel.Error); } public override void Log(LogLevel logLevel, TState state, Exception? exception, Func formatter) @@ -73,19 +69,18 @@ public override void Log(LogLevel logLevel, TState state, Exception? exc var message = GenerateMessage(formatter(state, exception), exception, logLevel); - if (logLevel >= LogLevel.Error) - { - context.ErrorOutputWriter.WriteLine(message); - GlobalContext.Current.OriginalConsoleError.WriteLine(message); - } - else - { - context.OutputWriter.WriteLine(message); - GlobalContext.Current.OriginalConsoleOut.WriteLine(message); - } + WriteToOutput(message, logLevel >= LogLevel.Error); } - private string GenerateMessage(string message, Exception? exception, LogLevel logLevel) + /// + /// Generates the formatted message to be logged. + /// Override this method to customize the message format. + /// + /// The message to log. + /// The exception associated with this log entry, if any. + /// The log level. + /// The formatted message. + protected virtual string GenerateMessage(string message, Exception? exception, LogLevel logLevel) { var stringBuilder = new StringBuilder(); @@ -120,4 +115,45 @@ private string GenerateMessage(string message, Exception? exception, LogLevel lo return builtString; } + + /// + /// Writes the message to the output. + /// Override this method to customize how messages are written. + /// + /// The formatted message to write. + /// True if this is an error-level message. + protected virtual void WriteToOutput(string message, bool isError) + { + if (isError) + { + context.ErrorOutputWriter.WriteLine(message); + GlobalContext.Current.OriginalConsoleError.WriteLine(message); + } + else + { + context.OutputWriter.WriteLine(message); + GlobalContext.Current.OriginalConsoleOut.WriteLine(message); + } + } + + /// + /// Asynchronously writes the message to the output. + /// Override this method to customize how messages are written. + /// + /// The formatted message to write. + /// True if this is an error-level message. + /// A task representing the async operation. + protected virtual async ValueTask WriteToOutputAsync(string message, bool isError) + { + if (isError) + { + await context.ErrorOutputWriter.WriteLineAsync(message); + await GlobalContext.Current.OriginalConsoleError.WriteLineAsync(message); + } + else + { + await context.OutputWriter.WriteLineAsync(message); + await GlobalContext.Current.OriginalConsoleOut.WriteLineAsync(message); + } + } } 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 316cd35c04..d17dfa1d33 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 @@ -2555,10 +2555,14 @@ namespace .Logging public class DefaultLogger : . { public DefaultLogger(.Context context) { } + protected .Context Context { get; } + protected virtual string GenerateMessage(string message, ? exception, . logLevel) { } public override void Log(. logLevel, TState state, ? exception, formatter) { } public override . LogAsync(. logLevel, TState state, ? exception, formatter) { } public void PushProperties(.> dictionary) { } public void PushProperty(string name, object? value) { } + protected virtual void WriteToOutput(string message, bool isError) { } + protected virtual . WriteToOutputAsync(string message, bool isError) { } } public interface ILogger { 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 6935605b9a..c8c1e43783 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 @@ -2555,10 +2555,14 @@ namespace .Logging public class DefaultLogger : . { public DefaultLogger(.Context context) { } + protected .Context Context { get; } + protected virtual string GenerateMessage(string message, ? exception, . logLevel) { } public override void Log(. logLevel, TState state, ? exception, formatter) { } public override . LogAsync(. logLevel, TState state, ? exception, formatter) { } public void PushProperties(.> dictionary) { } public void PushProperty(string name, object? value) { } + protected virtual void WriteToOutput(string message, bool isError) { } + protected virtual . WriteToOutputAsync(string message, bool isError) { } } public interface ILogger { 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 a0aa39ff4d..9c65473252 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 @@ -2555,10 +2555,14 @@ namespace .Logging public class DefaultLogger : . { public DefaultLogger(.Context context) { } + protected .Context Context { get; } + protected virtual string GenerateMessage(string message, ? exception, . logLevel) { } public override void Log(. logLevel, TState state, ? exception, formatter) { } public override . LogAsync(. logLevel, TState state, ? exception, formatter) { } public void PushProperties(.> dictionary) { } public void PushProperty(string name, object? value) { } + protected virtual void WriteToOutput(string message, bool isError) { } + protected virtual . WriteToOutputAsync(string message, bool isError) { } } public interface ILogger { 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 520c6e603f..9035d876c9 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 @@ -2477,10 +2477,14 @@ namespace .Logging public class DefaultLogger : . { public DefaultLogger(.Context context) { } + protected .Context Context { get; } + protected virtual string GenerateMessage(string message, ? exception, . logLevel) { } public override void Log(. logLevel, TState state, ? exception, formatter) { } public override . LogAsync(. logLevel, TState state, ? exception, formatter) { } public void PushProperties(.> dictionary) { } public void PushProperty(string name, object? value) { } + protected virtual void WriteToOutput(string message, bool isError) { } + protected virtual . WriteToOutputAsync(string message, bool isError) { } } public interface ILogger { diff --git a/docs/docs/advanced/performance-best-practices.md b/docs/docs/advanced/performance-best-practices.md index 15034990fd..98502dc6de 100644 --- a/docs/docs/advanced/performance-best-practices.md +++ b/docs/docs/advanced/performance-best-practices.md @@ -20,7 +20,7 @@ TUnit's AOT (Ahead-of-Time) compilation mode provides the best performance for t ``` :::performance Native AOT Performance -TUnit with Native AOT compilation delivers exceptional speed improvements - benchmarks show **11.65x faster** execution compared to regular JIT. See the [AOT benchmarks](/docs/benchmarks) for detailed measurements. +TUnit with Native AOT compilation delivers significant speed improvements compared to regular JIT. See the [benchmarks](/docs/benchmarks) for detailed measurements. ::: Benefits: diff --git a/docs/docs/customization-extensibility/logging.md b/docs/docs/customization-extensibility/logging.md index 90c072ece3..8dddaee561 100644 --- a/docs/docs/customization-extensibility/logging.md +++ b/docs/docs/customization-extensibility/logging.md @@ -29,3 +29,78 @@ dotnet run --log-level Warning ``` The above will show only logs that are `Warning` or higher (e.g. `Error`, `Critical`) while executing the test. + +## Custom Loggers + +The `DefaultLogger` class is designed to be extensible. You can inherit from it to customize message formatting and output behavior. + +### Available Extension Points + +- `Context` - Protected property to access the associated context +- `GenerateMessage(string message, Exception? exception, LogLevel logLevel)` - Override to customize message formatting +- `WriteToOutput(string message, bool isError)` - Override to customize how messages are written +- `WriteToOutputAsync(string message, bool isError)` - Async version of WriteToOutput + +### Example: Adding Test Headers + +Here's an example of a custom logger that prepends a test identifier header before the first log message: + +```csharp +public class TestHeaderLogger : DefaultLogger +{ + private bool _hasOutputHeader; + + public TestHeaderLogger(Context context) : base(context) { } + + protected override string GenerateMessage(string message, Exception? exception, LogLevel logLevel) + { + var baseMessage = base.GenerateMessage(message, exception, logLevel); + + if (!_hasOutputHeader && Context is TestContext testContext) + { + _hasOutputHeader = true; + var testId = $"{testContext.TestDetails.ClassType.Name}.{testContext.TestDetails.TestName}"; + return $"--- {testId} ---\n{baseMessage}"; + } + + return baseMessage; + } +} +``` + +### Using Custom Loggers + +Create an instance of your custom logger and use it directly: + +```csharp +[Test] +public async Task MyTest() +{ + var logger = new TestHeaderLogger(TestContext.Current!); + logger.LogInformation("This message will have a test header"); + logger.LogInformation("Subsequent messages won't repeat the header"); +} +``` + +### Example: Custom Output Destinations + +You can override the write methods to send output to additional destinations: + +```csharp +public class MultiDestinationLogger : DefaultLogger +{ + private readonly TextWriter _additionalOutput; + + public MultiDestinationLogger(Context context, TextWriter additionalOutput) + : base(context) + { + _additionalOutput = additionalOutput; + } + + protected override void WriteToOutput(string message, bool isError) + { + base.WriteToOutput(message, isError); + _additionalOutput.WriteLine(message); + } +} +``` diff --git a/docs/docs/migration/mstest.md b/docs/docs/migration/mstest.md index 5559f6433f..63086eec62 100644 --- a/docs/docs/migration/mstest.md +++ b/docs/docs/migration/mstest.md @@ -1,7 +1,7 @@ # Migrating from MSTest :::from-mstest Performance Boost -Migrating from MSTest to TUnit can significantly improve test execution speed. Benchmarks show TUnit is **1.3x faster** than MSTest on average. Check the [detailed benchmarks](/docs/benchmarks) to see performance comparisons. +Migrating from MSTest to TUnit can improve test execution speed. Check the [benchmarks](/docs/benchmarks) to see how TUnit compares. ::: ## Quick Reference diff --git a/docs/docs/migration/nunit.md b/docs/docs/migration/nunit.md index 89c484d31f..1ad0ee3b28 100644 --- a/docs/docs/migration/nunit.md +++ b/docs/docs/migration/nunit.md @@ -1,7 +1,7 @@ # Migrating from NUnit :::from-nunit Performance Boost -Migrating from NUnit to TUnit can significantly improve test execution speed. Benchmarks show TUnit is **1.2x faster** than NUnit on average. Check the [detailed benchmarks](/docs/benchmarks) to see performance comparisons. +Migrating from NUnit to TUnit can improve test execution speed. Check the [benchmarks](/docs/benchmarks) to see how TUnit compares. ::: ## Quick Reference diff --git a/docs/docs/migration/xunit.md b/docs/docs/migration/xunit.md index c2e64b36a6..368614b2c3 100644 --- a/docs/docs/migration/xunit.md +++ b/docs/docs/migration/xunit.md @@ -1,7 +1,7 @@ # Migrating from xUnit.net :::from-xunit Performance Boost -Migrating from xUnit to TUnit can significantly improve test execution speed. Benchmarks show TUnit is **1.3x faster** than xUnit v3 on average. Check the [detailed benchmarks](/docs/benchmarks) to see performance comparisons. +Migrating from xUnit to TUnit can improve test execution speed. Check the [benchmarks](/docs/benchmarks) to see how TUnit compares. ::: ## Quick Reference