diff --git a/TUnit.Engine/Xml/JUnitXmlWriter.cs b/TUnit.Engine/Xml/JUnitXmlWriter.cs
index 1c9156d241..9d3a2d77c1 100644
--- a/TUnit.Engine/Xml/JUnitXmlWriter.cs
+++ b/TUnit.Engine/Xml/JUnitXmlWriter.cs
@@ -10,6 +10,39 @@ namespace TUnit.Engine.Xml;
internal static class JUnitXmlWriter
{
+ ///
+ /// Sanitizes a string to be XML-safe by removing invalid XML characters.
+ /// According to XML 1.0 spec, valid characters are:
+ /// #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
+ ///
+ private static string SanitizeForXml(string? value)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ return string.Empty;
+ }
+
+ // At this point, value is guaranteed to not be null
+ var builder = new StringBuilder(value!.Length);
+ foreach (var ch in value!)
+ {
+ // Check if character is valid according to XML 1.0 spec
+ if (ch == 0x9 || ch == 0xA || ch == 0xD ||
+ (ch >= 0x20 && ch <= 0xD7FF) ||
+ (ch >= 0xE000 && ch <= 0xFFFD))
+ {
+ builder.Append(ch);
+ }
+ else
+ {
+ // Replace invalid character with its hex representation
+ builder.Append($"[0x{((int)ch):X}]");
+ }
+ }
+
+ return builder.ToString();
+ }
+
public static string GenerateXml(
IEnumerable testUpdates,
string? filter)
@@ -49,7 +82,7 @@ public static string GenerateXml(
//
xmlWriter.WriteStartElement("testsuites");
- xmlWriter.WriteAttributeString("name", assemblyName);
+ xmlWriter.WriteAttributeString("name", SanitizeForXml(assemblyName));
xmlWriter.WriteAttributeString("tests", summary.Total.ToString(CultureInfo.InvariantCulture));
xmlWriter.WriteAttributeString("failures", summary.Failures.ToString(CultureInfo.InvariantCulture));
xmlWriter.WriteAttributeString("errors", summary.Errors.ToString(CultureInfo.InvariantCulture));
@@ -75,14 +108,14 @@ private static void WriteTestSuite(
string? filter)
{
writer.WriteStartElement("testsuite");
- writer.WriteAttributeString("name", assemblyName);
+ writer.WriteAttributeString("name", SanitizeForXml(assemblyName));
writer.WriteAttributeString("tests", summary.Total.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("failures", summary.Failures.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("errors", summary.Errors.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("skipped", summary.Skipped.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("time", summary.TotalTime.TotalSeconds.ToString("F3", CultureInfo.InvariantCulture));
writer.WriteAttributeString("timestamp", summary.Timestamp.ToString("o", CultureInfo.InvariantCulture));
- writer.WriteAttributeString("hostname", Environment.MachineName);
+ writer.WriteAttributeString("hostname", SanitizeForXml(Environment.MachineName));
// Write properties
WriteProperties(writer, targetFramework, filter);
@@ -102,24 +135,24 @@ private static void WriteProperties(XmlWriter writer, string targetFramework, st
writer.WriteStartElement("property");
writer.WriteAttributeString("name", "framework");
- writer.WriteAttributeString("value", targetFramework);
+ writer.WriteAttributeString("value", SanitizeForXml(targetFramework));
writer.WriteEndElement();
writer.WriteStartElement("property");
writer.WriteAttributeString("name", "platform");
- writer.WriteAttributeString("value", RuntimeInformation.OSDescription);
+ writer.WriteAttributeString("value", SanitizeForXml(RuntimeInformation.OSDescription));
writer.WriteEndElement();
writer.WriteStartElement("property");
writer.WriteAttributeString("name", "runtime");
- writer.WriteAttributeString("value", RuntimeInformation.FrameworkDescription);
+ writer.WriteAttributeString("value", SanitizeForXml(RuntimeInformation.FrameworkDescription));
writer.WriteEndElement();
if (!string.IsNullOrEmpty(filter))
{
writer.WriteStartElement("property");
writer.WriteAttributeString("name", "filter");
- writer.WriteAttributeString("value", filter);
+ writer.WriteAttributeString("value", SanitizeForXml(filter));
writer.WriteEndElement();
}
@@ -152,8 +185,8 @@ private static void WriteTestCase(XmlWriter writer, TestNodeUpdateMessage test)
// Write testcase element
writer.WriteStartElement("testcase");
- writer.WriteAttributeString("name", testName);
- writer.WriteAttributeString("classname", className);
+ writer.WriteAttributeString("name", SanitizeForXml(testName));
+ writer.WriteAttributeString("classname", SanitizeForXml(className));
writer.WriteAttributeString("time", duration.TotalSeconds.ToString("F3", CultureInfo.InvariantCulture));
// Write state-specific child elements
@@ -198,16 +231,16 @@ private static void WriteFailure(XmlWriter writer, FailedTestNodeStateProperty f
var exception = failed.Exception;
if (exception != null)
{
- writer.WriteAttributeString("message", exception.Message);
+ writer.WriteAttributeString("message", SanitizeForXml(exception.Message));
writer.WriteAttributeString("type", exception.GetType().FullName ?? "AssertionException");
- writer.WriteString(exception.ToString());
+ writer.WriteString(SanitizeForXml(exception.ToString()));
}
else
{
var message = failed.Explanation ?? "Test failed";
- writer.WriteAttributeString("message", message);
+ writer.WriteAttributeString("message", SanitizeForXml(message));
writer.WriteAttributeString("type", "TestFailedException");
- writer.WriteString(message);
+ writer.WriteString(SanitizeForXml(message));
}
writer.WriteEndElement(); // failure
@@ -220,16 +253,16 @@ private static void WriteError(XmlWriter writer, ErrorTestNodeStateProperty erro
var exception = error.Exception;
if (exception != null)
{
- writer.WriteAttributeString("message", exception.Message);
+ writer.WriteAttributeString("message", SanitizeForXml(exception.Message));
writer.WriteAttributeString("type", exception.GetType().FullName ?? "Exception");
- writer.WriteString(exception.ToString());
+ writer.WriteString(SanitizeForXml(exception.ToString()));
}
else
{
var message = error.Explanation ?? "Test error";
- writer.WriteAttributeString("message", message);
+ writer.WriteAttributeString("message", SanitizeForXml(message));
writer.WriteAttributeString("type", "TestErrorException");
- writer.WriteString(message);
+ writer.WriteString(SanitizeForXml(message));
}
writer.WriteEndElement(); // error
@@ -239,9 +272,9 @@ private static void WriteTimeoutError(XmlWriter writer, TimeoutTestNodeStateProp
{
writer.WriteStartElement("error");
var message = timeout.Explanation ?? "Test timed out";
- writer.WriteAttributeString("message", message);
+ writer.WriteAttributeString("message", SanitizeForXml(message));
writer.WriteAttributeString("type", "TimeoutException");
- writer.WriteString(message);
+ writer.WriteString(SanitizeForXml(message));
writer.WriteEndElement(); // error
}
@@ -267,8 +300,8 @@ private static void WriteSkipped(XmlWriter writer, SkippedTestNodeStateProperty
{
writer.WriteStartElement("skipped");
var message = skipped.Explanation ?? "Test skipped";
- writer.WriteAttributeString("message", message);
- writer.WriteString(message);
+ writer.WriteAttributeString("message", SanitizeForXml(message));
+ writer.WriteString(SanitizeForXml(message));
writer.WriteEndElement(); // skipped
}
diff --git a/TUnit.TestProject/JUnitReporterInvalidXmlCharacterTests.cs b/TUnit.TestProject/JUnitReporterInvalidXmlCharacterTests.cs
new file mode 100644
index 0000000000..e7c1b7e103
--- /dev/null
+++ b/TUnit.TestProject/JUnitReporterInvalidXmlCharacterTests.cs
@@ -0,0 +1,41 @@
+using TUnit.Core;
+
+namespace TUnit.TestProject;
+
+public class JUnitReporterInvalidXmlCharacterTests
+{
+ [Test]
+ [Arguments("Some valid string")]
+ public async Task TestWithValidString(string parameter)
+ {
+ await Assert.That(parameter).IsNotNull();
+ }
+
+ [Test]
+ [Arguments("A string with an invalid \x04 character")]
+ public async Task TestWithInvalidXmlCharacter(string parameter)
+ {
+ await Assert.That(parameter).IsNotNull();
+ }
+
+ [Test]
+ [Arguments("Multiple\x01\x02\x03\x04invalid characters")]
+ public async Task TestWithMultipleInvalidXmlCharacters(string parameter)
+ {
+ await Assert.That(parameter).IsNotNull();
+ }
+
+ [Test]
+ [Arguments("Test\twith\nvalid\rcontrol characters")]
+ public async Task TestWithValidControlCharacters(string parameter)
+ {
+ await Assert.That(parameter).IsNotNull();
+ }
+
+ [Test]
+ public async Task TestFailingWithInvalidCharacterInException()
+ {
+ // This test intentionally fails with an exception message containing invalid XML characters
+ throw new InvalidOperationException("Error with invalid \x04 character in exception message");
+ }
+}