Skip to content

Commit 4e90344

Browse files
authored
fix: escape name for control chars in generated xml (#30)
* Use the latest Spekt.TestLogger drop with common xml utilities * Document logger options supported with core logger Fixes #25
1 parent 38a6816 commit 4e90344

File tree

6 files changed

+73
-51
lines changed

6 files changed

+73
-51
lines changed

CHANGELOG.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
## Unreleased (v3.0.x)
44

5-
* Refactor to support [core testlogger][]
6-
* Compatibility: minimum framework is netstandard1.5 and TestPlatform 15.5.0
7-
* Use test run start and end times for run duration reporting for assembly. See #26
5+
- Refactor to support [core testlogger][]
6+
- Compatibility: minimum framework is netstandard1.5 and TestPlatform 15.5.0
7+
- Use test run start and end times for run duration reporting for assembly. See #26
8+
- Escape control characters from the generated xml. See #25
9+
- Token expansion for `{assembly}` and `{framework}` in results file. See
10+
https://github.com/spekt/testlogger/wiki/Logger-Configuration
811

912
[core testlogger]: https://github.com/spekt/testlogger

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,9 @@ A path for the report file can be specified as follows:
3434

3535
**Note:** the arguments to `--logger` should be in quotes since `;` is treated as a command delimiter in shell.
3636

37+
All common options to the logger is documented [in the wiki][config-wiki].
38+
39+
[config-wiki]: https://github.com/spekt/testlogger/wiki/Logger-Configuration
40+
3741
## License
3842
MIT

scripts/dependencies.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<MSTestVersion>1.3.2</MSTestVersion>
55
<NETTestSdkVersion>15.7.2</NETTestSdkVersion>
66
<MoqVersion>4.9.0</MoqVersion>
7-
<TestLoggerVersion>3.0.26</TestLoggerVersion>
7+
<TestLoggerVersion>3.0.28</TestLoggerVersion>
88

99
<!-- Test Assets use the minimum supported versions -->
1010
<NETTestSdkMinimumVersion>15.5.0</NETTestSdkMinimumVersion>

src/Xunit.Xml.TestLogger/XunitXmlSerializer.cs

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ namespace Microsoft.VisualStudio.TestPlatform.Extension.Xunit.Xml.TestLogger
99
using System.IO;
1010
using System.Linq;
1111
using System.Text;
12-
using System.Text.RegularExpressions;
1312
using System.Xml.Linq;
1413
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
1514
using Spekt.TestLogger.Core;
15+
using Spekt.TestLogger.Utilities;
1616

1717
public class XunitXmlSerializer : ITestResultSerializer
1818
{
@@ -166,8 +166,8 @@ private static XElement CreateErrorElement(TestResultInfo result)
166166
private static XElement CreateFailureElement(string exceptionType, string message, string stackTrace)
167167
{
168168
XElement failureElement = new XElement("failure", new XAttribute("exception-type", exceptionType));
169-
failureElement.Add(new XElement("message", RemoveInvalidXmlChar(message)));
170-
failureElement.Add(new XElement("stack-trace", RemoveInvalidXmlChar(stackTrace)));
169+
failureElement.Add(new XElement("message", message.ReplaceInvalidXmlChar()));
170+
failureElement.Add(new XElement("stack-trace", stackTrace.ReplaceInvalidXmlChar()));
171171

172172
return failureElement;
173173
}
@@ -248,7 +248,7 @@ private static XElement CreateTestElement(TestResultInfo result)
248248
{
249249
var element = new XElement(
250250
"test",
251-
new XAttribute("name", result.Name),
251+
new XAttribute("name", result.Name.ReplaceInvalidXmlChar()),
252252
new XAttribute("type", result.FullTypeName),
253253
new XAttribute("method", result.Method),
254254
new XAttribute("time", result.Duration.TotalSeconds.ToString("F7", CultureInfo.InvariantCulture)),
@@ -264,13 +264,13 @@ private static XElement CreateTestElement(TestResultInfo result)
264264
else if (m.Category == "skipReason")
265265
{
266266
// Using the self-defined category skipReason for now
267-
element.Add(new XElement("reason", new XCData(RemoveInvalidXmlChar(m.Text))));
267+
element.Add(new XElement("reason", new XCData(m.Text.ReplaceInvalidXmlChar())));
268268
}
269269
}
270270

271271
if (!string.IsNullOrWhiteSpace(stdOut.ToString()))
272272
{
273-
element.Add(new XElement("output", RemoveInvalidXmlChar(stdOut.ToString())));
273+
element.Add(new XElement("output", stdOut.ToString().ReplaceInvalidXmlChar()));
274274
}
275275

276276
var fileName = result.TestCase.CodeFilePath;
@@ -284,8 +284,8 @@ private static XElement CreateTestElement(TestResultInfo result)
284284
{
285285
element.Add(new XElement(
286286
"failure",
287-
new XElement("message", RemoveInvalidXmlChar(result.ErrorMessage)),
288-
new XElement("stack-trace", RemoveInvalidXmlChar(result.ErrorStackTrace))));
287+
new XElement("message", result.ErrorMessage.ReplaceInvalidXmlChar()),
288+
new XElement("stack-trace", result.ErrorStackTrace.ReplaceInvalidXmlChar())));
289289
}
290290

291291
if (result.Traits != null)
@@ -315,27 +315,5 @@ private static string OutcomeToString(TestOutcome outcome)
315315
return "Unknown";
316316
}
317317
}
318-
319-
private static string RemoveInvalidXmlChar(string str)
320-
{
321-
if (str != null)
322-
{
323-
// From xml spec (http://www.w3.org/TR/xml/#charsets) valid chars:
324-
// #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
325-
326-
// we are handling only #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
327-
// because C# support unicode character in range \u0000 to \uFFFF
328-
MatchEvaluator evaluator = new MatchEvaluator(ReplaceInvalidCharacterWithUniCodeEscapeSequence);
329-
string invalidChar = @"[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD]";
330-
return Regex.Replace(str, invalidChar, evaluator);
331-
}
332-
333-
return str;
334-
}
335-
336-
private static string ReplaceInvalidCharacterWithUniCodeEscapeSequence(Match match)
337-
{
338-
return string.Format(@"\u{0:x4}", (ushort)match.Value[0]);
339-
}
340318
}
341319
}

test/Xunit.Xml.TestLogger.AcceptanceTests/TestResultsXmlTests.cs

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
namespace Xunit.Xml.TestLogger.AcceptanceTests
55
{
66
using System;
7+
using System.Collections.Generic;
78
using System.Globalization;
89
using System.IO;
910
using System.Linq;
@@ -133,15 +134,15 @@ public void AssemblyElementTotalAttributeShouldValueEqualToNumberOfTotalTests()
133134
{
134135
XmlNode assemblyNode = this.testResultsXmlDocument.SelectSingleNode(TestResultsXmlTests.AssemblyElement);
135136

136-
Assert.Equal("5", assemblyNode.Attributes["total"].Value);
137+
Assert.Equal("6", assemblyNode.Attributes["total"].Value);
137138
}
138139

139140
[Fact]
140141
public void AssemblyElementPassedAttributeShouldValueEqualToNumberOfPassedTests()
141142
{
142143
XmlNode assemblyNode = this.testResultsXmlDocument.SelectSingleNode(TestResultsXmlTests.AssemblyElement);
143144

144-
Assert.Equal("2", assemblyNode.Attributes["passed"].Value);
145+
Assert.Equal("3", assemblyNode.Attributes["passed"].Value);
145146
}
146147

147148
[Fact]
@@ -192,7 +193,7 @@ public void CollectionElementsCountShouldBeTwo()
192193
{
193194
XmlNodeList collectionElementNodeList = this.testResultsXmlDocument.SelectNodes(TestResultsXmlTests.CollectionElement);
194195

195-
Assert.True(collectionElementNodeList.Count == 2);
196+
Assert.Equal(3, collectionElementNodeList.Count);
196197
}
197198

198199
[Fact]
@@ -237,13 +238,25 @@ public void CollectionElementTimeAttributeShouldHaveValidFormatValue()
237238
}
238239

239240
[Fact]
240-
public void CollectionElementShouldContainThreeTestsElemets()
241+
public void CollectionElementShouldContainThreeTestsElements()
241242
{
242243
XmlNode unitTest1Collection = this.GetUnitTest1Collection();
243244

244245
Assert.True(unitTest1Collection.SelectNodes("test").Count == 3);
245246
}
246247

248+
[Fact]
249+
public void TestElementNameAttributeShouldBeEscaped()
250+
{
251+
var testNodes = this.GetTestXmlNodePartial(
252+
"UnitTest3",
253+
@"Xunit.Xml.TestLogger.NetCore.Tests.UnitTest3.TestInvalidName");
254+
255+
Assert.Equal(
256+
"Xunit.Xml.TestLogger.NetCore.Tests.UnitTest3.TestInvalidName(input: \"Head\\u0080r\")",
257+
testNodes.Item(0).Attributes["name"].Value);
258+
}
259+
247260
[Fact]
248261
public void TestElementTypeAttributeShouldHaveCorrectValue()
249262
{
@@ -290,7 +303,9 @@ public void FailedTestElementResultAttributeShouldHaveValueFail()
290303
[Fact]
291304
public void PassedTestElementResultAttributeShouldHaveValuePass()
292305
{
293-
XmlNode passedTestXmlNode = this.GetATestXmlNode("Xunit.Xml.TestLogger.NetCore.Tests.UnitTest1.PassTest11");
306+
XmlNode passedTestXmlNode = this.GetATestXmlNode(
307+
"UnitTest1",
308+
"Xunit.Xml.TestLogger.NetCore.Tests.UnitTest1.PassTest11");
294309

295310
Assert.Equal("Pass", passedTestXmlNode.Attributes["result"].Value);
296311
}
@@ -317,7 +332,9 @@ public void FailedTestElementShouldContainsFailureDetails()
317332
[Fact]
318333
public void SkippedTestElementShouldContainSkippingReason()
319334
{
320-
XmlNode skippedTestNode = this.GetATestXmlNode("Xunit.Xml.TestLogger.NetCore.Tests.UnitTest1.SkipTest11");
335+
XmlNode skippedTestNode = this.GetATestXmlNode(
336+
"UnitTest1",
337+
"Xunit.Xml.TestLogger.NetCore.Tests.UnitTest1.SkipTest11");
321338
var reasonNodes = skippedTestNode.SelectNodes("reason");
322339

323340
Assert.Equal(1, reasonNodes.Count);
@@ -331,27 +348,38 @@ public void SkippedTestElementShouldContainSkippingReason()
331348
Assert.Equal(expectedReason, reasonData.Value);
332349
}
333350

334-
private XmlNode GetATestXmlNode(string queryTestName = "Xunit.Xml.TestLogger.NetCore.Tests.UnitTest1.FailTest11")
351+
private XmlNode GetATestXmlNode(
352+
string collectionName = "UnitTest1",
353+
string queryTestName = "Xunit.Xml.TestLogger.NetCore.Tests.UnitTest1.FailTest11")
335354
{
336-
XmlNode unitTest1Collection = this.GetUnitTest1Collection();
355+
var unitTest1Collection = this.GetUnitTestCollection(collectionName);
337356

338357
var testNodes = unitTest1Collection.SelectNodes($"test[@name=\"{queryTestName}\"]");
339358
return testNodes.Item(0);
340359
}
341360

342-
private XmlNode GetUnitTest1Collection()
361+
private XmlNodeList GetTestXmlNodePartial(
362+
string collectionName,
363+
string testName)
343364
{
344-
XmlNodeList collectionElementNodeList =
345-
this.testResultsXmlDocument.SelectNodes(TestResultsXmlTests.CollectionElement);
365+
var unitTest1Collection = this.GetUnitTestCollection(collectionName);
346366

347-
Assert.True(collectionElementNodeList.Count == 2);
367+
var testNodes = unitTest1Collection.SelectNodes($"test[contains(@name, \"{testName}\")]");
368+
return testNodes;
369+
}
348370

349-
XmlNode unitTest1Collection = null;
350-
var firstCollectionName = collectionElementNodeList[0].Attributes["name"];
351-
unitTest1Collection = "Test collection for Xunit.Xml.TestLogger.NetCore.Tests.UnitTest1".Equals(firstCollectionName.Value)
352-
? collectionElementNodeList[0] : collectionElementNodeList[1];
371+
private XmlNode GetUnitTestCollection(string name)
372+
{
373+
var testNodes = this.testResultsXmlDocument.SelectNodes(
374+
$"//assemblies/assembly/collection[contains(@name, \"{name}\")]");
353375

354-
return unitTest1Collection;
376+
Assert.Equal(1, testNodes.Count);
377+
return testNodes.Item(0);
378+
}
379+
380+
private XmlNode GetUnitTest1Collection()
381+
{
382+
return this.GetUnitTestCollection("UnitTest1");
355383
}
356384
}
357385
}

test/assets/Xunit.Xml.TestLogger.NetCore.Tests/UnitTest1.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,13 @@ public void FailTest22()
3636
Assert.False(true);
3737
}
3838
}
39+
40+
public class UnitTest3
41+
{
42+
[Theory]
43+
[InlineData("Head\x80r")] // See issue #25
44+
public void TestInvalidName(string input)
45+
{
46+
}
47+
}
3948
}

0 commit comments

Comments
 (0)