Skip to content
Closed
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
18 changes: 6 additions & 12 deletions TUnit.Core/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,13 @@ public void AddAsyncLocalValues()

public virtual string GetStandardOutput()
{
if (_outputBuilder.Length == 0)
{
return string.Empty;
}

_outputLock.EnterReadLock();

try
{
return _outputBuilder.ToString();
return _outputBuilder.Length == 0
? string.Empty
: _outputBuilder.ToString();
}
finally
{
Expand All @@ -101,16 +98,13 @@ public virtual string GetStandardOutput()

public virtual string GetErrorOutput()
{
if (_errorOutputBuilder.Length == 0)
{
return string.Empty;
}

_errorOutputLock.EnterReadLock();

try
{
return _errorOutputBuilder.ToString();
return _errorOutputBuilder.Length == 0
? string.Empty
: _errorOutputBuilder.ToString();
}
finally
{
Expand Down
41 changes: 37 additions & 4 deletions TUnit.Engine/Logging/OptimizedConsoleInterceptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,17 +165,50 @@ public override async Task WriteLineAsync(string? value)

#if NET
public override void Write(ReadOnlySpan<char> buffer) => Write(new string(buffer));
public override void Write(StringBuilder? value) => Write(value?.ToString() ?? string.Empty);
public override void Write(StringBuilder? value) => Write(CopyStringBuilderSafely(value));
public override Task WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken = new())
=> WriteAsync(new string(buffer.Span));
public override Task WriteAsync(StringBuilder? value, CancellationToken cancellationToken = new())
=> WriteAsync(value?.ToString() ?? string.Empty);
=> WriteAsync(CopyStringBuilderSafely(value));
public override void WriteLine(ReadOnlySpan<char> buffer) => WriteLine(new string(buffer));
public override void WriteLine(StringBuilder? value) => WriteLine(value?.ToString() ?? string.Empty);
public override void WriteLine(StringBuilder? value) => WriteLine(CopyStringBuilderSafely(value));
public override Task WriteLineAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken = new())
=> WriteLineAsync(new string(buffer.Span));
public override Task WriteLineAsync(StringBuilder? value, CancellationToken cancellationToken = new())
=> WriteLineAsync(value?.ToString() ?? string.Empty);
=> WriteLineAsync(CopyStringBuilderSafely(value));

/// <summary>
/// Safely copies the content of a caller-owned StringBuilder into a string.
/// Callers (e.g., ASP.NET Core's ConsoleLogger) may pool and reuse their
/// StringBuilder after Write returns. If the StringBuilder is mutated
/// concurrently during the copy, the ArgumentOutOfRangeException is caught
/// and the output for that single log entry is lost rather than crashing
/// the test.
/// </summary>
private static string CopyStringBuilderSafely(StringBuilder? value)
{
if (value is null)
{
return string.Empty;
}

try
{
int length = value.Length;
if (length == 0)
{
return string.Empty;
}

return string.Create(length, value, static (span, sb) => sb.CopyTo(0, span, span.Length));
}
catch (ArgumentOutOfRangeException)
{
// The caller's StringBuilder was mutated concurrently (e.g., returned
// to a pool and reused by another thread). Swallow rather than crash.
return string.Empty;
}
}
#endif

public override IFormatProvider FormatProvider => GetOriginalOut().FormatProvider;
Expand Down