Skip to content
Merged
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
117 changes: 71 additions & 46 deletions TUnit.Core/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,16 @@ private ReaderWriterLockSlim GetOutputLock() =>
private ReaderWriterLockSlim GetErrorOutputLock() =>
LazyInitializer.EnsureInitialized(ref _errorOutputLock, static () => new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion))!;

[field: AllowNull, MaybeNull]
public TextWriter OutputWriter => field ??= new ConcurrentStringWriter(GetOutputBuilder(), GetOutputLock());
private ConcurrentStringWriter? _outputWriter;
private ConcurrentStringWriter? _errorOutputWriter;
private ConcurrentStringWriter GetOutputWriter() =>
LazyInitializer.EnsureInitialized(ref _outputWriter, () => new ConcurrentStringWriter(GetOutputBuilder(), GetOutputLock()))!;
private ConcurrentStringWriter GetErrorOutputWriter() =>
LazyInitializer.EnsureInitialized(ref _errorOutputWriter, () => new ConcurrentStringWriter(GetErrorOutputBuilder(), GetErrorOutputLock()))!;

[field: AllowNull, MaybeNull]
public TextWriter ErrorOutputWriter => field ??= new ConcurrentStringWriter(GetErrorOutputBuilder(), GetErrorOutputLock());
public TextWriter OutputWriter => GetOutputWriter();

public TextWriter ErrorOutputWriter => GetErrorOutputWriter();

// Internal accessors for console interceptor line buffers
internal ConsoleLineBuffer ConsoleStdOutLineBuffer =>
Expand Down Expand Up @@ -101,49 +106,9 @@ public void AddAsyncLocalValues()
#endif
}

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

var outputLock = GetOutputLock();
outputLock.EnterReadLock();

try
{
return _outputBuilder.Length == 0
? string.Empty
: _outputBuilder.ToString();
}
finally
{
outputLock.ExitReadLock();
}
}
public virtual string GetStandardOutput() => _outputWriter?.GetContent() ?? string.Empty;

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

var errorOutputLock = GetErrorOutputLock();
errorOutputLock.EnterReadLock();

try
{
return _errorOutputBuilder.Length == 0
? string.Empty
: _errorOutputBuilder.ToString();
}
finally
{
errorOutputLock.ExitReadLock();
}
}
public virtual string GetErrorOutput() => _errorOutputWriter?.GetContent() ?? string.Empty;

public DefaultLogger GetDefaultLogger()
{
Expand All @@ -167,8 +132,17 @@ public void Dispose()
/// </summary>
internal sealed class ConcurrentStringWriter : TextWriter
{
private const int MaxOutputLength = 1_048_576; // 1M chars (~2MB)

// Trim to 75% of max to avoid re-trimming on every subsequent write
private const int TrimTarget = MaxOutputLength * 3 / 4;

private static readonly string TruncationNotice =
$"[... output truncated — exceeded {MaxOutputLength:N0} character limit, showing most recent output ...]{Environment.NewLine}";

private readonly StringBuilder _builder;
private readonly ReaderWriterLockSlim _lock;
private bool _truncated;

public ConcurrentStringWriter(StringBuilder builder, ReaderWriterLockSlim lockSlim)
{
Expand All @@ -184,6 +158,7 @@ public override void Write(char value)
try
{
_builder.Append(value);
TrimIfNeeded();
}
finally
{
Expand All @@ -199,6 +174,7 @@ public override void Write(string? value)
try
{
_builder.Append(value);
TrimIfNeeded();
}
finally
{
Expand All @@ -215,6 +191,7 @@ public override void Write(char[] buffer, int index, int count)
try
{
_builder.Append(buffer, index, count);
TrimIfNeeded();
}
finally
{
Expand All @@ -229,6 +206,7 @@ public override void WriteLine()
try
{
_builder.AppendLine();
TrimIfNeeded();
}
finally
{
Expand All @@ -242,6 +220,7 @@ public override void WriteLine(string? value)
try
{
_builder.AppendLine(value);
TrimIfNeeded();
}
finally
{
Expand All @@ -257,6 +236,7 @@ public override void Write(char[]? buffer)
try
{
_builder.Append(buffer);
TrimIfNeeded();
}
finally
{
Expand All @@ -271,6 +251,7 @@ public override void Write(bool value)
try
{
_builder.Append(value);
TrimIfNeeded();
}
finally
{
Expand All @@ -284,6 +265,7 @@ public override void Write(int value)
try
{
_builder.Append(value);
TrimIfNeeded();
}
finally
{
Expand All @@ -297,6 +279,7 @@ public override void Write(uint value)
try
{
_builder.Append(value);
TrimIfNeeded();
}
finally
{
Expand All @@ -310,6 +293,7 @@ public override void Write(long value)
try
{
_builder.Append(value);
TrimIfNeeded();
}
finally
{
Expand All @@ -323,6 +307,7 @@ public override void Write(ulong value)
try
{
_builder.Append(value);
TrimIfNeeded();
}
finally
{
Expand All @@ -336,6 +321,7 @@ public override void Write(float value)
try
{
_builder.Append(value);
TrimIfNeeded();
}
finally
{
Expand All @@ -349,6 +335,7 @@ public override void Write(double value)
try
{
_builder.Append(value);
TrimIfNeeded();
}
finally
{
Expand All @@ -362,6 +349,7 @@ public override void Write(decimal value)
try
{
_builder.Append(value);
TrimIfNeeded();
}
finally
{
Expand All @@ -377,6 +365,7 @@ public override void Write(object? value)
try
{
_builder.Append(value);
TrimIfNeeded();
}
finally
{
Expand All @@ -385,6 +374,42 @@ public override void Write(object? value)
}
}

private void TrimIfNeeded()
{
if (_builder.Length > MaxOutputLength)
{
var removeCount = _builder.Length - TrimTarget;

// Avoid splitting a surrogate pair at the trim boundary
if (removeCount > 0 && char.IsHighSurrogate(_builder[removeCount - 1]))
{
removeCount--;
}

_builder.Remove(0, removeCount);
_truncated = true;
}
}

internal string GetContent()
{
_lock.EnterReadLock();
try
{
if (_builder.Length == 0)
{
return string.Empty;
}

var content = _builder.ToString();
return _truncated ? string.Concat(TruncationNotice, content) : content;
}
finally
{
_lock.ExitReadLock();
}
}

public override void Flush()
{
// StringBuilder doesn't need flushing
Expand Down
Loading