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
78 changes: 57 additions & 21 deletions TUnit.Core/Logging/DefaultLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ public class DefaultLogger(Context context) : TUnitLogger
{
private readonly ConcurrentDictionary<string, List<string>> _values = new();

/// <summary>
/// Gets the context associated with this logger.
/// </summary>
protected Context Context => context;

public void PushProperties(IDictionary<string, List<object>> dictionary)
{
foreach (var keyValuePair in dictionary)
Expand Down Expand Up @@ -52,16 +57,7 @@ public override async ValueTask LogAsync<TState>(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<TState>(LogLevel logLevel, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
Expand All @@ -73,19 +69,18 @@ public override void Log<TState>(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)
/// <summary>
/// Generates the formatted message to be logged.
/// Override this method to customize the message format.
/// </summary>
/// <param name="message">The message to log.</param>
/// <param name="exception">The exception associated with this log entry, if any.</param>
/// <param name="logLevel">The log level.</param>
/// <returns>The formatted message.</returns>
protected virtual string GenerateMessage(string message, Exception? exception, LogLevel logLevel)
{
var stringBuilder = new StringBuilder();

Expand Down Expand Up @@ -120,4 +115,45 @@ private string GenerateMessage(string message, Exception? exception, LogLevel lo

return builtString;
}

/// <summary>
/// Writes the message to the output.
/// Override this method to customize how messages are written.
/// </summary>
/// <param name="message">The formatted message to write.</param>
/// <param name="isError">True if this is an error-level message.</param>
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);
}
}
Comment on lines +125 to +137
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WriteToOutput method references the 'context' parameter directly instead of using the protected Context property. This creates an inconsistency since a protected Context property was added specifically for derived classes to access the context. Consider using 'Context' instead of 'context' for consistency with the extensibility design.

Copilot uses AI. Check for mistakes.

/// <summary>
/// Asynchronously writes the message to the output.
/// Override this method to customize how messages are written.
/// </summary>
/// <param name="message">The formatted message to write.</param>
/// <param name="isError">True if this is an error-level message.</param>
/// <returns>A task representing the async operation.</returns>
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);
}
}
Comment on lines +146 to +158
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WriteToOutputAsync method references the 'context' parameter directly instead of using the protected Context property. This creates an inconsistency since a protected Context property was added specifically for derived classes to access the context. Consider using 'Context' instead of 'context' for consistency with the extensibility design.

Copilot uses AI. Check for mistakes.
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<TState>(. logLevel, TState state, ? exception, <TState, ?, string> formatter) { }
public override . LogAsync<TState>(. logLevel, TState state, ? exception, <TState, ?, string> formatter) { }
public void PushProperties(.<string, .<object>> 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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TState>(. logLevel, TState state, ? exception, <TState, ?, string> formatter) { }
public override . LogAsync<TState>(. logLevel, TState state, ? exception, <TState, ?, string> formatter) { }
public void PushProperties(.<string, .<object>> 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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TState>(. logLevel, TState state, ? exception, <TState, ?, string> formatter) { }
public override . LogAsync<TState>(. logLevel, TState state, ? exception, <TState, ?, string> formatter) { }
public void PushProperties(.<string, .<object>> 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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TState>(. logLevel, TState state, ? exception, <TState, ?, string> formatter) { }
public override . LogAsync<TState>(. logLevel, TState state, ? exception, <TState, ?, string> formatter) { }
public void PushProperties(.<string, .<object>> 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
{
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/advanced/performance-best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
75 changes: 75 additions & 0 deletions docs/docs/customization-extensibility/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
```
2 changes: 1 addition & 1 deletion docs/docs/migration/mstest.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/migration/nunit.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/migration/xunit.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading