Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 8, 2026

The JUnit reporter crashes with ArgumentException when test arguments or exception messages contain invalid XML 1.0 characters (e.g., \x04), terminating the test run.

Changes

  • Added SanitizeForXml method to validate characters against XML 1.0 spec and replace invalid characters with hex notation [0xNN]
  • Applied sanitization to all string outputs: test names, class names, exception messages, stack traces, and metadata (assembly names, hostnames, filters, etc.)
  • Preserved valid control characters: tab (0x9), newline (0xA), carriage return (0xD) pass through as XML entities

Example

Before (crashes):

[Test]
[Arguments("A string with an invalid \x04 character")]
public async Task TestSomething(string parameter) { }

After (generates valid XML):

<testcase name="TestSomething(A string with an invalid [0x4] character)" .../>

Exception messages are similarly sanitized:

<error message="Error with invalid [0x4] character">...</error>
Original prompt

This section details on the original issue you should resolve

<issue_title>[Bug]: JUnit reporter crashes if test arguments contains an "invalid character"</issue_title>
<issue_description>### Description

When running a test of the form

[Test]
[Arguments("Some valid string")]
[Arguments("A string with an invalid \x04 character")]
public async Task TestSomething(string parameter)
{
   await Assert.That(parameter).IsNotNull();  
}

The test runner will crash with the following exception: Error output: Unhandled exception in AppDomain: System.ArgumentException: '�', hexadecimal value 0x04, is an invalid character.

Expected Behavior

The invalid character should be escaped in the resulting name / the runner should not crash.

Actual Behavior

The runner crashed when running with the JUnit reporter active. The tests behave fine when running from an IDE or without reporting.

Steps to Reproduce

  1. Create a test class with the following test:
[Test]
[Arguments("Some valid string")]
[Arguments("A string with an invalid \x04 character")]
public async Task TestSomething(string parameter)
{
   await Assert.That(parameter).IsNotNull();  
}
  1. Build the associated test project.
  2. Run the tests using a command of the form dotnet test --test-modules <Path to test> --report-trx --results-directory <Path to results>
  3. Observe that the test runner crashes

TUnit Version

1.9.26

.NET Version

.NET 10

Operating System

Linux

IDE / Test Runner

dotnet CLI (dotnet test / dotnet run)

Error Output / Stack Trace

Exit code: -532462766
  Error output: Unhandled exception in AppDomain: System.ArgumentException: '', hexadecimal value 0x04, is an invalid character.
     at System.Xml.XmlEncodedRawTextWriter.WriteAttributeTextBlock(Char* pSrc, Char* pSrcEnd)
     at System.Xml.XmlEncodedRawTextWriter.WriteString(String text)
     at System.Xml.XmlWellFormedWriter.WriteString(String text)
     at System.Xml.XmlWriter.WriteAttributeString(String localName, String value)
     at TUnit.Engine.Xml.JUnitXmlWriter.WriteTestCase(XmlWriter writer, TestNodeUpdateMessage test)
     at TUnit.Engine.Xml.JUnitXmlWriter.WriteTestSuite(XmlWriter writer, IEnumerable`1 tests, String assemblyName, String targetFramework, TestSummary summary, String filter)
     at TUnit.Engine.Xml.JUnitXmlWriter.GenerateXml(IEnumerable`1 testUpdates, String filter)
     at TUnit.Engine.Reporters.JUnitReporter.AfterRunAsync(Int32 exitCode, CancellationToken cancellation)
     at Microsoft.Testing.Platform.Hosts.CommonHost.RunTestAppAsync(CancellationToken testApplicationCancellationToken) in /_/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs:line 122
     at Microsoft.Testing.Platform.Hosts.CommonHost.RunAsync() in /_/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs:line 54
     at Microsoft.Testing.Platform.Hosts.CommonHost.RunAsync() in /_/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs:line 62
     at Microsoft.Testing.Platform.Hosts.CommonHost.RunAsync() in /_/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs:line 74
     at Microsoft.Testing.Platform.Builder.TestApplication.RunAsync() in /_/src/Platform/Microsoft.Testing.Platform/Builder/TestApplication.cs:line 227
...
  Unhandled exception. System.ArgumentException: '', hexadecimal value 0x04, is an invalid character.
     at System.Xml.XmlEncodedRawTextWriter.WriteAttributeTextBlock(Char* pSrc, Char* pSrcEnd)
     at System.Xml.XmlEncodedRawTextWriter.WriteString(String text)
     at System.Xml.XmlWellFormedWriter.WriteString(String text)
     at System.Xml.XmlWriter.WriteAttributeString(String localName, String value)
     at TUnit.Engine.Xml.JUnitXmlWriter.WriteTestCase(XmlWriter writer, TestNodeUpdateMessage test)
     at TUnit.Engine.Xml.JUnitXmlWriter.WriteTestSuite(XmlWriter writer, IEnumerable`1 tests, String assemblyName, String targetFramework, TestSummary summary, String filter)
     at TUnit.Engine.Xml.JUnitXmlWriter.GenerateXml(IEnumerable`1 testUpdates, String filter)
     at TUnit.Engine.Reporters.JUnitReporter.AfterRunAsync(Int32 exitCode, CancellationToken cancellation)
     at Microsoft.Testing.Platform.Hosts.CommonHost.RunTestAppAsync(CancellationToken testApplicationCancellationToken) in /_/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs:line 122
     at Microsoft.Testing.Platform.Hosts.CommonHost.RunAsync() in /_/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs:line 54
     at Microsoft.Testing.Platform.Hosts.CommonHost.RunAsync() in /_/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs:line 62
     at Microsoft.Testing.Platform.Hosts.CommonHost.RunAsync() in /_/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs:line 74
     at Microsoft.Testing.Platform.Builder.TestApplication.RunAsync() in /_/src/Platform/Microsoft.Testing.Platform/Builder/TestApplication.cs:line 227
...

Add...


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Fix JUnit reporter crash for invalid characters in test arguments Fix JUnit reporter crash on invalid XML characters in test names and exceptions Jan 8, 2026
Copilot AI requested a review from thomhurst January 8, 2026 12:30
@thomhurst thomhurst marked this pull request as ready for review January 8, 2026 13:42
Copilot AI review requested due to automatic review settings January 8, 2026 13:42
@thomhurst thomhurst merged commit 6941bc1 into main Jan 8, 2026
12 of 13 checks passed
@thomhurst thomhurst deleted the copilot/fix-junit-reporter-crash branch January 8, 2026 13:42
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a critical bug where the JUnit reporter crashes with ArgumentException when test arguments or exception messages contain invalid XML 1.0 characters (such as \x04), which was terminating the entire test run.

Key Changes:

  • Added a SanitizeForXml method that validates characters against the XML 1.0 specification and replaces invalid characters with hex notation [0xNN]
  • Applied sanitization to all string outputs in the JUnit XML writer including test names, class names, exception messages, stack traces, and metadata
  • Added integration tests to verify the fix works for various scenarios including multiple invalid characters and exception messages

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
TUnit.Engine/Xml/JUnitXmlWriter.cs Implements SanitizeForXml method and applies it to all XML string outputs to prevent crashes from invalid XML characters
TUnit.TestProject/JUnitReporterInvalidXmlCharacterTests.cs Adds integration tests covering invalid XML characters in test arguments and exception messages
Comments suppressed due to low confidence (1)

TUnit.Engine/Xml/JUnitXmlWriter.cs:350

  • These 'if' statements can be combined.
                if (timing.GlobalTiming.StartTime is { } startTime)
                {
                    if (earliestStartTime is null || startTime < earliestStartTime)
                    {
                        earliestStartTime = startTime;
                    }
                }

Comment on lines +27 to +40
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}]");
}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SanitizeForXml method does not correctly handle Unicode surrogate pairs. When iterating character-by-character, characters in the range U+10000 to U+10FFFF (which are valid per XML 1.0 spec) are represented as two char values (high and low surrogates). The current implementation will incorrectly process each surrogate individually, potentially replacing valid characters with hex notation. Consider using StringInfo or Rune enumeration to properly handle multi-char Unicode sequences.

Suggested change
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}]");
}
var length = value!.Length;
for (var i = 0; i < length; i++)
{
var ch = value[i];
int codePoint;
var charsConsumed = 1;
// Handle surrogate pairs to correctly process characters in the range U+10000 to U+10FFFF
if (char.IsHighSurrogate(ch) && i + 1 < length && char.IsLowSurrogate(value[i + 1]))
{
codePoint = char.ConvertToUtf32(ch, value[i + 1]);
charsConsumed = 2;
}
else
{
codePoint = ch;
}
// Check if character is valid according to XML 1.0 spec
var isValid =
codePoint == 0x9 ||
codePoint == 0xA ||
codePoint == 0xD ||
(codePoint >= 0x20 && codePoint <= 0xD7FF) ||
(codePoint >= 0xE000 && codePoint <= 0xFFFD) ||
(codePoint >= 0x10000 && codePoint <= 0x10FFFF);
if (isValid)
{
if (charsConsumed == 1)
{
builder.Append((char)codePoint);
}
else
{
// Append the original surrogate pair
builder.Append(ch);
builder.Append(value[i + 1]);
}
}
else
{
// Replace invalid character (or unpaired surrogate) with its hex representation
builder.Append($"[0x{codePoint:X}]");
}
if (charsConsumed == 2)
{
i++;
}

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +27
var builder = new StringBuilder(value!.Length);
foreach (var ch in value!)
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The redundant null-forgiving operators (!) on lines 26 and 27 are unnecessary since the check on line 20 already confirms the value is not null or empty. The value cannot be null at this point in the execution.

Suggested change
var builder = new StringBuilder(value!.Length);
foreach (var ch in value!)
var builder = new StringBuilder(value.Length);
foreach (var ch in value)

Copilot uses AI. Check for mistakes.

namespace TUnit.TestProject;

public class JUnitReporterInvalidXmlCharacterTests
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test class is missing Category attributes. According to TUnit's testing approach, tests in TUnit.TestProject should be properly categorized (e.g., "Pass" or "Fail") to enable filtered test execution. The TestFailingWithInvalidCharacterInException test intentionally fails and should have a [Category("Fail")] attribute. The other tests that pass should have [Category("Pass")] attributes. This enables proper filtering when running the test suite.

Copilot uses AI. Check for mistakes.
{
writer.WriteAttributeString("message", exception.Message);
writer.WriteAttributeString("message", SanitizeForXml(exception.Message));
writer.WriteAttributeString("type", exception.GetType().FullName ?? "AssertionException");
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception type names from exception.GetType().FullName are written to XML without sanitization. While standard .NET type names won't contain invalid XML characters, custom exception types could theoretically have non-ASCII or problematic characters in their namespace or type names. For consistency and defensive programming, these should also be sanitized.

Copilot uses AI. Check for mistakes.
{
writer.WriteAttributeString("message", exception.Message);
writer.WriteAttributeString("message", SanitizeForXml(exception.Message));
writer.WriteAttributeString("type", exception.GetType().FullName ?? "Exception");
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception type names from exception.GetType().FullName are written to XML without sanitization. While standard .NET type names won't contain invalid XML characters, custom exception types could theoretically have non-ASCII or problematic characters in their namespace or type names. For consistency and defensive programming, these should also be sanitized.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +44
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();
}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SanitizeForXml method lacks dedicated unit tests. While integration tests exist in TUnit.TestProject, there should be unit tests in TUnit.Engine.Tests that directly test the XML sanitization logic with various edge cases (null, empty, valid control characters, invalid characters, surrogate pairs, boundary conditions). This would ensure the sanitization function works correctly in isolation.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: JUnit reporter crashes if test arguments contains an "invalid character"

2 participants