Skip to content

Commit

Permalink
Optimize progress reporting by reusing StringBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
lahma committed Apr 25, 2023
1 parent ccc7a34 commit 8e2762e
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 166 deletions.
121 changes: 17 additions & 104 deletions src/NUnitFramework/framework/Interfaces/TNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
#nullable enable

using System;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Xml;

Expand Down Expand Up @@ -48,7 +47,7 @@ public TNode(string name)
public TNode(string name, string? value, bool valueIsCDATA)
: this(name)
{
Value = EscapeInvalidXmlCharacters(value);
Value = XmlExtensions.EscapeInvalidXmlCharacters(value);
ValueIsCDATA = valueIsCDATA;
}

Expand Down Expand Up @@ -86,26 +85,30 @@ public TNode(string name, string? value, bool valueIsCDATA)
/// </summary>
public TNode? FirstChild => ChildNodes.Count == 0 ? null : ChildNodes[0];

[ThreadStatic]
private static StringBuilder? _outerXmlStringBuilder;

/// <summary>
/// Gets the XML representation of this node.
/// </summary>
public string OuterXml
{
get
{
var stringWriter = new System.IO.StringWriter();
var settings = new XmlWriterSettings();
settings.ConformanceLevel = ConformanceLevel.Fragment;

using (XmlWriter xmlWriter = XmlWriter.Create(stringWriter, settings))
{
WriteTo(xmlWriter);
}

return stringWriter.ToString();
var stringBuilder = _outerXmlStringBuilder ??= new StringBuilder();
stringBuilder.Clear();
GetOuterXml(stringBuilder);
return stringBuilder.ToString();
}
}

internal void GetOuterXml(StringBuilder stringBuilder)
{
using var stringWriter = new System.IO.StringWriter(stringBuilder);
using var xmlWriter = XmlWriter.Create(stringWriter, XmlExtensions.FragmentWriterSettings);
WriteTo(xmlWriter);
}

#endregion

#region Static Methods
Expand Down Expand Up @@ -172,7 +175,7 @@ public TNode AddElementWithCDATA(string name, string value)
/// <param name="value">The value of the attribute.</param>
public void AddAttribute(string name, string value)
{
Attributes.Add(name, EscapeInvalidXmlCharacters(value));
Attributes.Add(name, XmlExtensions.EscapeInvalidXmlCharacters(value));
}

/// <summary>
Expand Down Expand Up @@ -217,7 +220,7 @@ public void WriteTo(XmlWriter writer)

if (Value != null)
if (ValueIsCDATA)
WriteCDataTo(writer);
writer.WriteCDataSafe(Value);
else
writer.WriteString(Value);

Expand Down Expand Up @@ -276,96 +279,6 @@ private static NodeList ApplySelection(NodeList nodeList, string xpath)
: resultNodes;
}

[return: NotNullIfNotNull("str")]
private static string? EscapeInvalidXmlCharacters(string? str)
{
if (str == null) return null;

StringBuilder? builder = null;
for (int i = 0; i < str.Length; i++)
{
char c = str[i];
if(c > 0x20 && c < 0x7F)
{
// ASCII characters - break quickly for these
builder?.Append(c);
}
// From the XML specification: https://www.w3.org/TR/xml/#charsets
// Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
// Any Unicode character, excluding the surrogate blocks, FFFE, and FFFF.
else if (!(0x0 <= c && c <= 0x8) &&
c != 0xB &&
c != 0xC &&
!(0xE <= c && c <= 0x1F) &&
!(0x7F <= c && c <= 0x84) &&
!(0x86 <= c && c <= 0x9F) &&
!(0xD800 <= c && c <= 0xDFFF) &&
c != 0xFFFE &&
c != 0xFFFF)
{
builder?.Append(c);
}
// Also check if the char is actually a high/low surrogate pair of two characters.
// If it is, then it is a valid XML character (from above based on the surrogate blocks).
else if (char.IsHighSurrogate(c) &&
i + 1 != str.Length &&
char.IsLowSurrogate(str[i + 1]))
{
if (builder != null)
{
builder.Append(c);
builder.Append(str[i + 1]);
}
i++;
}
else
{
// We keep the builder null so that we don't allocate a string
// when doing this conversion until we encounter a unicode character.
// Then, we allocate the rest of the string and escape the invalid
// character.
if (builder == null)
{
builder = new StringBuilder();
for (int index = 0; index < i; index++)
builder.Append(str[index]);
}
builder.Append(CharToUnicodeSequence(c));
}
}

if (builder != null)
return builder.ToString();
else
return str;
}

private static string CharToUnicodeSequence(char symbol)
{
return $"\\u{(int)symbol:x4}";
}

private void WriteCDataTo(XmlWriter writer)
{
int start = 0;
string text = Value ?? throw new InvalidOperationException();

while (true)
{
int illegal = text.IndexOf("]]>", start, StringComparison.Ordinal);
if (illegal < 0)
break;
writer.WriteCData(text.Substring(start, illegal - start + 2));
start = illegal + 2;
if (start >= text.Length)
return;
}

if (start > 0)
writer.WriteCData(text.Substring(start));
else
writer.WriteCData(text);
}

#endregion

Expand Down
36 changes: 20 additions & 16 deletions src/NUnitFramework/framework/Interfaces/TestMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

using System;
using System.Diagnostics;
using System.IO;
using System.Xml;

namespace NUnit.Framework.Interfaces
{
Expand All @@ -22,18 +24,8 @@ public sealed class TestMessage
/// <param name="testId">ID of the test that produced the message</param>
public TestMessage(string destination, string text, string testId)
{
if (destination == null)
{
throw new ArgumentNullException(nameof(destination));
}

if (text == null)
{
throw new ArgumentNullException(nameof(text));
}

Destination = destination;
Message = text;
Destination = destination ?? throw new ArgumentNullException(nameof(destination));
Message = text ?? throw new ArgumentNullException(nameof(text));
TestId = testId;
}

Expand Down Expand Up @@ -65,15 +57,27 @@ public override string ToString()
/// </summary>
public string ToXml()
{
TNode tnode = new TNode("test-message", Message, true);
using var stringWriter = new StringWriter();
using (var writer = XmlWriter.Create(stringWriter, XmlExtensions.FragmentWriterSettings))
{
ToXml(writer);
}
return stringWriter.ToString();
}

internal void ToXml(XmlWriter writer)
{
writer.WriteStartElement("test-message");

if (Destination != null)
tnode.AddAttribute("destination", Destination);
writer.WriteAttributeString("destination", Destination);

if (TestId != null)
tnode.AddAttribute("testid", TestId);
writer.WriteAttributeString("testid", TestId);

writer.WriteCDataSafe(Message);

return tnode.OuterXml;
writer.WriteEndElement();
}
}
}
29 changes: 22 additions & 7 deletions src/NUnitFramework/framework/Interfaces/TestOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

#nullable enable

using System.IO;
using System.Xml;

namespace NUnit.Framework.Interfaces
{
/// <summary>
/// The TestOutput class holds a unit of output from
/// a test to a specific output stream
/// </summary>
public class TestOutput
{
public class TestOutput
{
/// <summary>
/// Construct with text, output destination type and
/// the name of the test that produced the output.
Expand Down Expand Up @@ -60,16 +63,28 @@ public override string ToString()
/// </summary>
public string ToXml()
{
TNode tnode = new TNode("test-output", Text, true);
using var stringWriter = new StringWriter();
using (var writer = XmlWriter.Create(stringWriter, XmlExtensions.FragmentWriterSettings))
{
ToXml(writer);
}
return stringWriter.ToString();
}

internal void ToXml(XmlWriter writer)
{
writer.WriteStartElement("test-output");
writer.WriteAttributeString("stream", Stream);

tnode.AddAttribute("stream", Stream);
if (TestId != null)
tnode.AddAttribute("testid", TestId);
writer.WriteAttributeString("testid", TestId);

if (TestName != null)
tnode.AddAttribute("testname", TestName);
writer.WriteAttributeStringSafe("testname", TestName);

writer.WriteCDataSafe(Text);

return tnode.OuterXml;
writer.WriteEndElement();
}
}
}
Loading

0 comments on commit 8e2762e

Please sign in to comment.