-
-
Notifications
You must be signed in to change notification settings - Fork 110
fix: prevent console output mixing between parallel tests #4549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
## Problem When running multiple tests in parallel, console output (Console.Write/WriteLine) was being mixed between tests or missing entirely. This occurred because the console interceptor's line buffer was a singleton instance field shared across all parallel tests. ## Root Cause OptimizedConsoleInterceptor used `private readonly StringBuilder _lineBuffer` which was shared by all tests. When Test A called Console.Write() without a newline, it buffered text in the shared buffer. If Test B then called Console.WriteLine(), it would append to the same buffer and route the combined output to Test B's context via Context.Current. ## Solution Move console line buffers into Context itself, leveraging the existing robust Context.Current AsyncLocal mechanism: - Added per-context console buffers in Context.cs with thread safety locks - Made GetLineBuffer() abstract, returning (StringBuilder Buffer, object Lock) - Added locking around all buffer operations for thread safety within tests - Updated StandardOutConsoleInterceptor and StandardErrorConsoleInterceptor ## Benefits - Per-test isolation: each test context has its own console line buffers - Thread safety: locks protect concurrent access within a single test - Consistency: follows same pattern as OutputWriter/ErrorOutputWriter - No new AsyncLocal: reuses existing Context.Current mechanism ## Testing - All 80 existing CaptureOutputTests pass - Added 33 new ParallelConsoleOutputTests for regression testing Fixes #4545 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
SummaryFixes console output mixing between parallel tests by moving line buffers from singleton interceptor instance to per-test Context. Critical IssuesThread-safe lazy initialization needed in Context.cs:30-34 The lazy initialization in internal (StringBuilder Buffer, object Lock) GetConsoleStdOutLineBuffer()
{
return (_consoleStdOutLineBuffer ??= new StringBuilder(), _consoleStdOutBufferLock);
}Problem: Multiple threads calling this simultaneously could each create their own StringBuilder before assignment completes, defeating the purpose of the fix. Solution: Either:
internal (StringBuilder Buffer, object Lock) GetConsoleStdOutLineBuffer()
{
if (_consoleStdOutLineBuffer == null)
{
lock (_consoleStdOutBufferLock)
{
if (_consoleStdOutLineBuffer == null)
{
_consoleStdOutLineBuffer = new StringBuilder();
}
}
}
return (_consoleStdOutLineBuffer, _consoleStdOutBufferLock);
}
Question: Can you clarify whether a single Context instance is guaranteed to only be accessed from one thread at a time, or if concurrent access is possible? SuggestionsNone - the approach is sound once the thread safety issue is resolved. Verdict |
Addresses post-merge feedback from PR #4549 regarding thread-safety concerns in console buffer lazy initialization. Replaced nullable StringBuilder fields with Lazy<StringBuilder> to guarantee thread-safe initialization when multiple threads access GetConsoleStdOutLineBuffer() or GetConsoleStdErrLineBuffer() concurrently. The previous implementation using ??= was not thread-safe and could result in multiple StringBuilder instances being created before assignment completes, undermining per-context buffer isolation. Benefits: - Thread-safe by default with Lazy<T> - No manual double-checked locking needed - Cleaner, more idiomatic C# code - AOT-compatible Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Addresses post-merge feedback from PR #4549 regarding thread-safety concerns in console buffer lazy initialization. Replaced nullable StringBuilder fields with Lazy<StringBuilder> to guarantee thread-safe initialization when multiple threads access GetConsoleStdOutLineBuffer() or GetConsoleStdErrLineBuffer() concurrently. The previous implementation using ??= was not thread-safe and could result in multiple StringBuilder instances being created before assignment completes, undermining per-context buffer isolation. Benefits: - Thread-safe by default with Lazy<T> - No manual double-checked locking needed - Cleaner, more idiomatic C# code - AOT-compatible Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Problem
When running multiple tests in parallel in Visual Studio Test Explorer (or via
dotnet test), console output fromConsole.Write()andConsole.WriteLine()was being mixed between tests or missing entirely.Reported in #4545
Root Cause
The
OptimizedConsoleInterceptorclass used an instance field_lineBufferto buffer partial console writes (e.g.,Console.Write()without a newline). SinceStandardOutConsoleInterceptorandStandardErrorConsoleInterceptorare singletons, this buffer was shared across all parallel tests.Example of the bug:
Console.Write("Test1-Start")→ buffered in shared_lineBufferConsole.WriteLine("-Test2-End")→ appends to same bufferContext.CurrentSolution
Moved console line buffers into
Contextitself, leveraging the existing robustContext.CurrentAsyncLocal mechanism:Changes:
TUnit.Core/Context.cs:
_consoleStdOutLineBuffer,_consoleStdErrLineBuffer)GetConsoleStdOutLineBuffer()andGetConsoleStdErrLineBuffer()methodsTUnit.Engine/Logging/OptimizedConsoleInterceptor.cs:
GetLineBuffer()abstract, returning(StringBuilder Buffer, object Lock)Console interceptor implementations:
Benefits
✅ Per-test isolation: Each test context has its own console line buffers
✅ Thread safety: Locks protect concurrent access within a single test
✅ Consistency: Follows same pattern as
OutputWriter/ErrorOutputWriter✅ No new AsyncLocal: Reuses existing
Context.CurrentmechanismTesting
CaptureOutputTestspassParallelConsoleOutputTestsfor regression testingCloses #4545
🤖 Generated with Claude Code