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
301 changes: 301 additions & 0 deletions TUnit.Core/Helpers/ValueStringBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace TUnit.Core.Helpers;

// From https://raw.githubusercontent.com/dotnet/runtime/refs/heads/main/src/libraries/Common/src/System/Text/ValueStringBuilder.cs
internal ref struct ValueStringBuilder
{
private char[]? _arrayToReturnToPool;
private Span<char> _chars;
private int _pos;

public ValueStringBuilder(Span<char> initialBuffer)
{
_arrayToReturnToPool = null;
_chars = initialBuffer;
_pos = 0;
}

public ValueStringBuilder(int initialCapacity)
{
_arrayToReturnToPool = ArrayPool<char>.Shared.Rent(initialCapacity);
_chars = _arrayToReturnToPool;
_pos = 0;
}

public int Length
{
get => _pos;
set
{
Debug.Assert(value >= 0);
Debug.Assert(value <= _chars.Length);
_pos = value;
}
}

public int Capacity => _chars.Length;

public void EnsureCapacity(int capacity)
{
// This is not expected to be called this with negative capacity
Debug.Assert(capacity >= 0);

// If the caller has a bug and calls this with negative capacity, make sure to call Grow to throw an exception.
if ((uint)capacity > (uint)_chars.Length)
Grow(capacity - _pos);
}

/// <summary>
/// Ensures that the builder is terminated with a NUL character.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void NullTerminate()
{
EnsureCapacity(_pos + 1);
_chars[_pos] = '\0';
}

/// <summary>
/// Get a pinnable reference to the builder.
/// Does not ensure there is a null char after <see cref="Length"/>
/// This overload is pattern matched in the C# 7.3+ compiler so you can omit
/// the explicit method call, and write eg "fixed (char* c = builder)"
/// </summary>
public ref char GetPinnableReference()
{
return ref MemoryMarshal.GetReference(_chars);
}

public ref char this[int index]
{
get
{
Debug.Assert(index < _pos);
return ref _chars[index];
}
}

public override string ToString()
{
string s = _chars.Slice(0, _pos).ToString();
Dispose();
return s;
}

/// <summary>Returns the underlying storage of the builder.</summary>
public Span<char> RawChars => _chars;

public ReadOnlySpan<char> AsSpan() => _chars.Slice(0, _pos);
public ReadOnlySpan<char> AsSpan(int start) => _chars.Slice(start, _pos - start);
public ReadOnlySpan<char> AsSpan(int start, int length) => _chars.Slice(start, length);

public void Insert(int index, char value, int count)
{
if (_pos > _chars.Length - count)
{
Grow(count);
}

int remaining = _pos - index;
_chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count));
_chars.Slice(index, count).Fill(value);
_pos += count;
}

public void Insert(int index, string? s)
{
if (s == null)
{
return;
}

int count = s.Length;

if (_pos > (_chars.Length - count))
{
Grow(count);
}

int remaining = _pos - index;
_chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count));
s
#if !NET
.AsSpan()
#endif
.CopyTo(_chars.Slice(index));
_pos += count;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(char c)
{
int pos = _pos;
Span<char> chars = _chars;
if ((uint)pos < (uint)chars.Length)
{
chars[pos] = c;
_pos = pos + 1;
}
else
{
GrowAndAppend(c);
}
}

public void Append(int value) => AppendSpanFormattable(value);

#if NET8_0_OR_GREATER
private void AppendSpanFormattable<T>(T value) where T : ISpanFormattable
{
Debug.Assert(typeof(T).Assembly.Equals(typeof(object).Assembly), "Implementation trusts the results of TryFormat because T is expected to be something known");

if (value.TryFormat(_chars, out int charsWritten, format: default, provider: null))
{
_pos += charsWritten;
return;
}

Append(value.ToString());
}
#else
private void AppendSpanFormattable<T>(T value)
{
Append(value!.ToString());
}
#endif


[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(string? s)
{
if (s == null)
{
return;
}

int pos = _pos;
if (s.Length == 1 && (uint)pos < (uint)_chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc.
{
_chars[pos] = s[0];
_pos = pos + 1;
}
else
{
AppendSlow(s);
}
}

private void AppendSlow(string s)
{
int pos = _pos;
if (pos > _chars.Length - s.Length)
{
Grow(s.Length);
}

s
#if !NET
.AsSpan()
#endif
.CopyTo(_chars.Slice(pos));
_pos += s.Length;
}

public void Append(char c, int count)
{
if (_pos > _chars.Length - count)
{
Grow(count);
}

Span<char> dst = _chars.Slice(_pos, count);
for (int i = 0; i < dst.Length; i++)
{
dst[i] = c;
}
_pos += count;
}

public void Append(scoped ReadOnlySpan<char> value)
{
int pos = _pos;
if (pos > _chars.Length - value.Length)
{
Grow(value.Length);
}

value.CopyTo(_chars.Slice(_pos));
_pos += value.Length;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<char> AppendSpan(int length)
{
int origPos = _pos;
if (origPos > _chars.Length - length)
{
Grow(length);
}

_pos = origPos + length;
return _chars.Slice(origPos, length);
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void GrowAndAppend(char c)
{
Grow(1);
Append(c);
}

/// <summary>
/// Resize the internal buffer either by doubling current buffer size or
/// by adding <paramref name="additionalCapacityBeyondPos"/> to
/// <see cref="_pos"/> whichever is greater.
/// </summary>
/// <param name="additionalCapacityBeyondPos">
/// Number of chars requested beyond current position.
/// </param>
[MethodImpl(MethodImplOptions.NoInlining)]
private void Grow(int additionalCapacityBeyondPos)
{
Debug.Assert(additionalCapacityBeyondPos > 0);
Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed.");

const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength

// Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try
// to double the size if possible, bounding the doubling to not go beyond the max array length.
int newCapacity = (int)Math.Max(
(uint)(_pos + additionalCapacityBeyondPos),
Math.Min((uint)_chars.Length * 2, ArrayMaxLength));

// Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative.
// This could also go negative if the actual required length wraps around.
char[] poolArray = ArrayPool<char>.Shared.Rent(newCapacity);

_chars.Slice(0, _pos).CopyTo(poolArray);

char[]? toReturn = _arrayToReturnToPool;
_chars = _arrayToReturnToPool = poolArray;
if (toReturn != null)
{
ArrayPool<char>.Shared.Return(toReturn);
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
char[]? toReturn = _arrayToReturnToPool;
this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again
if (toReturn != null)
{
ArrayPool<char>.Shared.Return(toReturn);
}
}
}
37 changes: 27 additions & 10 deletions TUnit.Core/TestContext.Output.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using TUnit.Core.Helpers;
using TUnit.Core.Interfaces;

namespace TUnit.Core;
Expand Down Expand Up @@ -75,29 +76,45 @@ public override string GetErrorOutput()

internal string GetOutput()
{
var vsb = new ValueStringBuilder(stackalloc char[256]);

var buildOutput = _buildTimeOutput ?? string.Empty;
var baseOutput = base.GetStandardOutput(); // Get output from base class (Context)
var writerOutput = _outputWriter?.ToString() ?? string.Empty;

// Combine all three sources: build-time, base class output, and writer output
var parts = new[] { buildOutput, baseOutput, writerOutput }
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
AppendIfNotNullOrEmpty(ref vsb, buildOutput);
AppendIfNotNullOrEmpty(ref vsb, baseOutput);
AppendIfNotNullOrEmpty(ref vsb, writerOutput);

return parts.Length == 0 ? string.Empty : string.Join(Environment.NewLine, parts);
return vsb.ToString();
}

internal string GetOutputError()
{
var vsb = new ValueStringBuilder(stackalloc char[256]);

var buildErrorOutput = _buildTimeErrorOutput ?? string.Empty;
var baseErrorOutput = base.GetErrorOutput(); // Get error output from base class (Context)
var writerErrorOutput = _errorWriter?.ToString() ?? string.Empty;

// Combine all three sources: build-time error, base class error output, and writer error output
var parts = new[] { buildErrorOutput, baseErrorOutput, writerErrorOutput }
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
AppendIfNotNullOrEmpty(ref vsb, buildErrorOutput);
AppendIfNotNullOrEmpty(ref vsb, baseErrorOutput);
AppendIfNotNullOrEmpty(ref vsb, writerErrorOutput);

return vsb.ToString();
}

private static void AppendIfNotNullOrEmpty(ref ValueStringBuilder builder, string? value)
{
if (string.IsNullOrEmpty(value))
{
return;
}

return parts.Length == 0 ? string.Empty : string.Join(Environment.NewLine, parts);
if (builder.Length > 0)
{
builder.Append(Environment.NewLine);
}
builder.Append(value);
}
}
Loading