Skip to content

[fix] Fix HTML logger exception on invalid XML chars in test display names - #16051

Merged
Jakub Jareลก (nohwnd) merged 2 commits into
mainfrom
fix/issue-10431-html-logger-special-chars-390ce20d95623f88
May 26, 2026
Merged

Jakub Jareลก (nohwnd) merged 2 commits into
mainfrom
fix/issue-10431-html-logger-special-chars-390ce20d95623f88

Conversation

@nohwnd

Copy link
Copy Markdown
Member

Fixes #10431

๐Ÿค– This is an automated fix generated by the Issue Triage & Auto-Fix workflow.

Root Cause

When a test's DisplayName (e.g. set via a [DataRow] attribute) contains XML 1.0 invalid control characters such as \x01โ€“\x08, \x0B, \x0C, \x0Eโ€“\x1F, DataContractSerializer.WriteObject() in HtmlLogger.PopulateHtmlFile() throws an XmlException:

Html Logger Error : '', hexadecimal value 0x01, is an invalid character.

The HTML report is silently not generated even though tests continue to pass. The XML 1.0 spec forbids these code points entirely โ€” they cannot appear even as character references.

Fix

Apply the same sanitization already used by TrxLogger's XmlPersistence.RemoveInvalidXmlChar(): replace XML-invalid characters with their Unicode escape representation (e.g. \u0001) before storing them in the HTML logger object model.

Added a private RemoveInvalidXmlChars(string?) helper to HtmlLogger and applied it to DisplayName, FullyQualifiedName, ErrorStackTrace, and ErrorMessage in TestResultHandler().

Test

Added unit test TestResultHandlerShouldSanitizeInvalidXmlCharsInDisplayName that verifies control characters (\x01, \x02, \x03) in DisplayName, ErrorMessage, and ErrorStackTrace are replaced with their \uXXXX escape sequences.

๐Ÿ” Triaged by Issue Repro Triage & Auto-Fix ๐Ÿ”

When a test's DisplayName (e.g. from a DataRow attribute) contains XML 1.0
invalid control characters such as 0x01-0x08, 0x0B, 0x0C, 0x0E-0x1F,
DataContractSerializer throws XmlException and silently prevents the HTML
report from being generated.

Apply the same sanitization pattern already used by TrxLogger's XmlPersistence
to replace invalid XML characters with their Unicode escape representation
(e.g. \u0001) before they are stored in the HTML logger object model.

Fixes #10431

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 22, 2026 01:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 failure mode in the HTML logger where DataContractSerializer.WriteObject() can throw when test result strings contain XML 1.0โ€“invalid control characters (e.g., from [DataRow] display names), preventing the HTML report from being generated.

Changes:

  • Sanitize DisplayName, FullyQualifiedName, ErrorMessage, and ErrorStackTrace in HtmlLogger.TestResultHandler() before storing them in the HTML logger object model.
  • Add RemoveInvalidXmlChars(string?) helper to escape invalid XML characters as \uXXXX.
  • Add a unit test validating escaping for control characters in result fields.

Reviewed changes

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

File Description
src/Microsoft.TestPlatform.Extensions.HtmlLogger/HtmlLogger.cs Adds XML-invalid character sanitization for key serialized fields and introduces a helper used during result handling.
test/Microsoft.TestPlatform.Extensions.HtmlLogger.UnitTests/HtmlLoggerTests.cs Adds coverage to ensure control characters are escaped instead of causing serialization failures.

Comment on lines +475 to +479
// From xml spec (http://www.w3.org/TR/xml/#charsets) valid chars:
// #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
const string invalidChar = @"[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD]";
return Regex.Replace(str, invalidChar, m => $@"\u{(ushort)m.Value[0]:x4}");
}
Comment on lines +459 to +467
/// <summary>
/// Removes characters that are invalid in XML 1.0 from a string.
/// </summary>
/// <remarks>
/// XML 1.0 valid characters: #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD].
/// Control characters in the range #x00-#x08, #x0B, #x0C, #x0E-#x1F are not valid and
/// will cause <see cref="DataContractSerializer"/> to throw an <see cref="System.Xml.XmlException"/>.
/// Invalid characters are replaced with their Unicode escape representation.
/// </remarks>

@nohwnd Jakub Jareลก (nohwnd) left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Review: Fix HTML logger exception on invalid XML chars

The fix correctly addresses the root cause โ€” DataContractSerializer throwing on XML 1.0 invalid control characters in test display names. Two issues worth addressing before merge:

  1. Correctness (surrogate pairs) โ€” The regex excludes the entire surrogate range \uD800โ€“\uDFFF, which means valid supplementary Unicode characters (emoji, etc.) in test display names are silently corrupted to \uD83D\uDE00-style sequences. Since DataContractSerializer handles valid surrogate pairs correctly, this sanitization is overly aggressive. See inline comment at line 476.

  2. Performance (static regex) โ€” The regex should be a static readonly compiled field rather than recreated per-call via the string overload. See inline comment at line 474.

The primary fix (replacing control chars \x00โ€“\x1F minus the whitespace ones) is correct and the test coverage is good.

๐Ÿง  Reviewed by Expert Code Reviewer

๐Ÿง  Reviewed by Expert Code Reviewer ๐Ÿง 

}

// From xml spec (http://www.w3.org/TR/xml/#charsets) valid chars:
// #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Correctness] Surrogate pairs (supplementary Unicode characters) are incorrectly stripped

The regex [^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD] has a gap between \uD7FF and \uE000 that covers the entire surrogate range \uD800โ€“\uDFFF. In .NET strings, supplementary Unicode characters (U+10000โ€“U+10FFFF, including all emoji) are encoded as surrogate pairs โ€” two char values both in that range.

DataContractSerializer / XmlWriter handle surrogate pairs correctly and serialize them as the corresponding supplementary Unicode code point, which is valid XML 1.0 ([#x10000-#x10FFFF]). So DataContractSerializer will not throw on a string like "Test(๐Ÿ˜€)".

The current code will corrupt such strings: "Test(๐Ÿ˜€)" โ†’ "Test(\uD83D\uDE00)" โ€” two broken escape sequences instead of the emoji. A test whose [DataRow] argument contains an emoji will have its display name silently mangled in the HTML report.

Suggested fix โ€” allow valid surrogate pairs to pass through by checking for a complete surrogate pair before replacing:

private static readonly Regex s_invalidXmlCharsRegex =
    new Regex(@"[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]",
              RegexOptions.Compiled);

private static string? RemoveInvalidXmlChars(string? str)
{
    if (str is null)
        return null;

    // Allow valid surrogate pairs (they represent U+10000โ€“U+10FFFF, which are valid XML chars).
    // Only lone surrogates are truly invalid.
    return s_invalidXmlCharsRegex.Replace(str, m =>
        m.Value.Length == 1 ? $@"\u{(ushort)m.Value[0]:x4}" : m.Value);
}

Alternatively, use char.IsHighSurrogate/char.IsLowSurrogate in a manual loop, which is clearer and avoids nested regex complexity.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed โ€” the surrogate range \uD800-\uDFFF is now excluded from the negated character class in the first alternative, so valid surrogate pairs pass through untouched. Two additional alternatives handle lone surrogates (unpaired high or low) using lookahead/lookbehind. Added a test that verifies "Test(๐Ÿ˜€)" is preserved as-is after sanitization.

๐Ÿ”ง Iterated by PR Iteration Agent ๐Ÿ”ง

{
return null;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Performance] Regex is re-instantiated on every call via the string overload

Regex.Replace(str, invalidChar, ...) with a const string pattern looks up (and possibly compiles) the regex on every invocation via .NET's internal cache (size 15). This method is called 4ร— per test result; for a run with thousands of tests this accumulates.

Since the project targets netstandard2.0 and net48, [GeneratedRegex] isn't available, but a static readonly field with RegexOptions.Compiled eliminates the cache lookup overhead:

private static readonly Regex s_invalidXmlCharsRegex = new Regex(
    @"[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD]",
    RegexOptions.Compiled);

Minor, but worth fixing given the hot-path nature of TestResultHandler.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed โ€” the regex is now a private static readonly Regex InvalidXmlCharsRegex with RegexOptions.Compiled, eliminating the per-call lookup overhead.

๐Ÿ”ง Iterated by PR Iteration Agent ๐Ÿ”ง

โ€ฆrs correctly

- Use a static readonly compiled Regex instead of re-creating on every call
- Exclude the surrogate range from the negated char class in the first
  alternative so valid surrogate pairs are not matched; add explicit
  lone-surrogate alternatives with lookahead/lookbehind to catch only
  invalid lone surrogates
- Add test verifying emoji (valid surrogate pair) passes through unchanged

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@nohwnd

Copy link
Copy Markdown
Member Author

Commit pushed: 82ef8e5

๐Ÿ”ง Iterated by PR Iteration Agent ๐Ÿ”ง

@nohwnd Jakub Jareลก (nohwnd) left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Both previous findings are resolved in this revision:

  • [Performance] Regex is now a private static readonly field with RegexOptions.Compiled โ€” no per-call overhead.
  • [Correctness] The regex now correctly handles surrogate pairs: valid high+low surrogate pairs pass through unchanged; only lone surrogates are replaced with their \uXXXX escape. The three-alternative pattern is logically correct, including the edge cases of a high surrogate at end-of-string and a low surrogate at start-of-string.

Test coverage is solid โ€” both the invalid control char case and the emoji (surrogate pair) preservation case are tested.

The fix is correct and complete. โœ…

๐Ÿง  Reviewed by Expert Code Reviewer ๐Ÿง 

๐Ÿง  Reviewed by Expert Code Reviewer ๐Ÿง 

@nohwnd
Jakub Jareลก (nohwnd) merged commit 80ab78a into main May 26, 2026
27 checks passed
@nohwnd
Jakub Jareลก (nohwnd) deleted the fix/issue-10431-html-logger-special-chars-390ce20d95623f88 branch May 26, 2026 11:52
This was referenced Jul 14, 2026
github-actions Bot pushed a commit to Nimblesite/SharpLsp that referenced this pull request Jul 14, 2026
Updated [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest)
from 18.7.0 to 18.8.0.

<details>
<summary>Release notes</summary>

_Sourced from [Microsoft.NET.Test.Sdk's
releases](https://github.com/microsoft/vstest/releases)._

## 18.8.0

## What's Changed
* Migrate from Newtonsoft.Json to System.Text.Json / Jsonite (merge to
main) by @โ€‹nohwnd in microsoft/vstest#15687
- For more detail refer to
https://devblogs.microsoft.com/dotnet/vs-test-is-removing-its-newtonsoft-json-dependency/
* Create source-only filter package by @โ€‹Youssef1313 in
microsoft/vstest#15638
   - Package release is pending.
* Add ARM64 msdia140.dll support to test platform packages by @โ€‹nohwnd
in microsoft/vstest#15692
* Fix mutex cleanup crash on macOS/Linux by @โ€‹nohwnd in
microsoft/vstest#15684
* Restrict artifact temp directory permissions on Unix by @โ€‹nohwnd in
microsoft/vstest#15729
* Add support for filtering uncategorized tests with TestCategory=None
by @โ€‹Evangelink in microsoft/vstest#15727
* Fix SCI binding failure in DTA hosts (main) by @โ€‹nohwnd in
microsoft/vstest#15724
* Fix HTML logger parallel file collision by @โ€‹nohwnd in
microsoft/vstest#15435
* Improve error message when testhost cannot be found by @โ€‹nohwnd in
microsoft/vstest#16053
* Fix HTML logger exception on invalid XML chars in test display names
by @โ€‹nohwnd in microsoft/vstest#16051

**Full Changelog**:
microsoft/vstest@v18.7.0...v18.8.0

Commits viewable in [compare
view](microsoft/vstest@v18.7.0...v18.8.0).
</details>

Updated [Serilog](https://github.com/serilog/serilog) from 4.3.1 to
4.4.0.

<details>
<summary>Release notes</summary>

_Sourced from [Serilog's
releases](https://github.com/serilog/serilog/releases)._

## 4.4.0

## What's Changed
* Emit SelfLog warning when extra arguments are provided by @โ€‹matantsach
in serilog/serilog#2222
* dont WriteQuotedJsonString for null by @โ€‹SimonCropp in
serilog/serilog#2216
* Pin System.Security.Cryptography.Xml to 8.0.3 in tests by @โ€‹ArieGato
in serilog/serilog#2232
* Route optional interfaces through OptionalInterfaceForwardingSink for
restricted sinks by @โ€‹ArieGato in
serilog/serilog#2234
* `SelfMetrics` by @โ€‹nblumhardt in
serilog/serilog#2237

## New Contributors
* @โ€‹matantsach made their first contribution in
serilog/serilog#2222
* @โ€‹ArieGato made their first contribution in
serilog/serilog#2232

**Full Changelog**:
serilog/serilog@v4.3.1...v4.4.0

Commits viewable in [compare
view](serilog/serilog@v4.3.1...v4.4.0).
</details>

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This was referenced Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dotnet test html logger throws execption when using special characters in DataRow attributes.

2 participants