Skip to content
Prev Previous commit
Next Next commit
fix: always show non-test output on console without duplication
Split console output responsibility between sinks:
- TestOutputSink: only captures TestContext output for test results,
  no-ops for all other contexts
- ConsoleOutputSink: always registered (not just --output Detailed).
  In detailed mode writes all output; in non-detailed mode only writes
  non-TestContext output (hooks, data source initialization, etc.)

This ensures infrastructure output (data source initialization, hooks)
is always visible on the real console regardless of output mode, while
test output only appears on console with --output Detailed. No
duplication occurs because each sink has a clear, non-overlapping
responsibility for non-test contexts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  • Loading branch information
thomhurst and claude committed Feb 14, 2026
commit 750eba6cf0f2163120bb59d3ba5eab9e71337316
13 changes: 6 additions & 7 deletions TUnit.Engine/Framework/TUnitServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,12 @@ public TUnitServiceProvider(IExtension extension,
// TestOutputSink: Always registered - accumulates to Context.OutputWriter/ErrorOutputWriter for test results
TUnitLoggerFactory.AddSink(new TestOutputSink());

// ConsoleOutputSink: For --output Detailed mode - real-time console output
if (VerbosityService.IsDetailedOutput)
{
TUnitLoggerFactory.AddSink(new ConsoleOutputSink(
StandardOutConsoleInterceptor.DefaultOut,
StandardErrorConsoleInterceptor.DefaultError));
}
// ConsoleOutputSink: Always registered - writes non-test output (hooks, data source
// initialization) to the real console. In --output Detailed mode, also writes test output.
TUnitLoggerFactory.AddSink(new ConsoleOutputSink(
StandardOutConsoleInterceptor.DefaultOut,
StandardErrorConsoleInterceptor.DefaultError,
VerbosityService.IsDetailedOutput));

// IdeStreamingSink: For IDE clients - real-time output streaming
// Disabled by default due to compatibility issues with Microsoft Testing Platform
Expand Down
13 changes: 11 additions & 2 deletions TUnit.Engine/Logging/ConsoleOutputSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,32 @@ namespace TUnit.Engine.Logging;

/// <summary>
/// A log sink that writes output to the actual console (stdout/stderr).
/// Only registered when --output Detailed is specified.
/// Always registered. In detailed mode, writes all output. In non-detailed mode,
/// only writes non-test output (hooks, data source initialization, etc.) so that
/// infrastructure output is always visible on the console.
/// </summary>
internal sealed class ConsoleOutputSink : ILogSink
{
private readonly TextWriter _stdout;
private readonly TextWriter _stderr;
private readonly bool _detailedOutput;

public ConsoleOutputSink(TextWriter stdout, TextWriter stderr)
public ConsoleOutputSink(TextWriter stdout, TextWriter stderr, bool detailedOutput)
{
_stdout = stdout;
_stderr = stderr;
_detailedOutput = detailedOutput;
}

public bool IsEnabled(LogLevel level) => true;

public void Log(LogLevel level, string message, Exception? exception, Context? context)
{
if (!_detailedOutput && context is TestContext)
{
return;
}

var writer = level >= LogLevel.Error ? _stderr : _stdout;
writer.WriteLine(message);
}
Expand Down
23 changes: 2 additions & 21 deletions TUnit.Engine/Logging/TestOutputSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ namespace TUnit.Engine.Logging;
/// A log sink that accumulates output to the test context's output writers.
/// Routes to OutputWriter for non-error levels and ErrorOutputWriter for error levels.
/// This captured output is included in test results.
/// Only captures output for TestContext — non-test contexts (hooks, data source
/// initialization, GlobalContext) are handled by ConsoleOutputSink when registered.
/// </summary>
internal sealed class TestOutputSink : ILogSink
{
Expand All @@ -16,19 +18,6 @@ public void Log(LogLevel level, string message, Exception? exception, Context? c
{
if (context is not TestContext)
{
// Only capture output for test contexts (included in test results).
// For non-test contexts (e.g. data source initialization, hooks, GlobalContext),
// write directly to the real console via the pre-interception writers
// to avoid recursion through the console interceptor and to ensure
// the output is visible rather than captured in an unread buffer.
if (context is not null)
{
var consoleWriter = level >= LogLevel.Error
? StandardErrorConsoleInterceptor.DefaultError
: StandardOutConsoleInterceptor.DefaultOut;
consoleWriter.WriteLine(message);
}

return;
}

Expand All @@ -40,14 +29,6 @@ public ValueTask LogAsync(LogLevel level, string message, Exception? exception,
{
if (context is not TestContext)
{
if (context is not null)
{
var consoleWriter = level >= LogLevel.Error
? StandardErrorConsoleInterceptor.DefaultError
: StandardOutConsoleInterceptor.DefaultOut;
consoleWriter.WriteLine(message);
}

return ValueTask.CompletedTask;
}

Expand Down