Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle illegal xml characters in test data #37

Merged
merged 52 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
5822047
Import code from vstest Microsoft.TestPlatform.Extensions.TrxLogger/X…
Siphonophora Mar 11, 2023
4f3ae94
Define interface for sanitizing input
Siphonophora Mar 11, 2023
776458d
Import json logic from appveyor logger
Siphonophora Mar 11, 2023
2cd1120
Setup proper sanitizer
Siphonophora Mar 11, 2023
f2827a2
Make test result info mostly immutable. Sanitize its contents in work…
Siphonophora Mar 11, 2023
07169d6
Sanitize traits
Siphonophora Mar 11, 2023
93fa03e
Add TestResultInfoBuilder, tests build
Siphonophora Mar 11, 2023
fcd09dc
Use builder
Siphonophora Mar 11, 2023
bae5673
Ensure sanatizer is there is needed
Siphonophora Mar 11, 2023
ddac633
Fix bug
Siphonophora Mar 11, 2023
d6c6cfc
Fix tests
Siphonophora Mar 11, 2023
52e7907
Fix broken test
Siphonophora Mar 11, 2023
887c3a3
Progress on fixing tests
Siphonophora Mar 11, 2023
c8ae005
Revert removing warnings as errors
Siphonophora Mar 11, 2023
184f661
Fix test
Siphonophora Mar 11, 2023
1bc4dc5
explicitly type assignment
Siphonophora Mar 11, 2023
0029581
Fix test
Siphonophora Mar 11, 2023
07ed44c
Remove fully qualified name. Escape some json
Siphonophora Mar 11, 2023
b23825e
Remove FQN, some json escaping
Siphonophora Mar 11, 2023
b46cc40
Remove fqn, json escaping
Siphonophora Mar 11, 2023
a4ed985
Use TestResult.TestCase.Traits not TestResult.Traits
Siphonophora Mar 14, 2023
7183aee
tabs to spaces
Siphonophora Mar 14, 2023
4d6a17b
Remove commented code
Siphonophora Mar 14, 2023
ee352d4
Remove old xml sanitizer
Siphonophora Mar 15, 2023
bc8f7a3
Move json sanitizer to test proj
Siphonophora Mar 18, 2023
b6f0f93
Remove unused, and probably invalid, Equals
Siphonophora Mar 18, 2023
1301a44
Clarify which display name we are using
Siphonophora Mar 18, 2023
0c60d74
Add the other display name.
Siphonophora Mar 18, 2023
d835e64
Add code file and line numbers
Siphonophora Mar 18, 2023
f5c7137
Update equals and hashcode
Siphonophora Mar 18, 2023
b008c91
Revert to using the TestCase display name here
Siphonophora Mar 18, 2023
f01bd36
Make DisplayName values internal
Siphonophora Mar 18, 2023
700d672
Fix test build
Siphonophora Mar 18, 2023
98cb901
Perf middle ground, compiled regex
Siphonophora Mar 18, 2023
e84024c
Encode 'discouraged' char
Siphonophora Mar 18, 2023
e0520bb
Add unit tests
Siphonophora Mar 18, 2023
2174a8a
Remove invalid test, make other test less bad
Siphonophora Mar 18, 2023
25e6bb3
Stop verification failures based on local env
Siphonophora Mar 18, 2023
ffe79ec
Remove unused display name
Siphonophora Mar 18, 2023
da7f7dc
Change regex precidence b/c gh action was failing
Siphonophora Mar 18, 2023
fb87a1d
Revert an incorrect verification
Siphonophora Mar 18, 2023
b589469
Fix other verification issues.
Siphonophora Mar 18, 2023
e1e6bb5
Remove char that shouldn't have been added to verification
Siphonophora Mar 18, 2023
2d51248
Fix verify failure on windows because of \r. Revert scrubber precidence
Siphonophora Mar 18, 2023
6de6c1a
Tabs to spaces
Siphonophora Apr 15, 2023
c11febb
Restore some test only functionality
Siphonophora Apr 15, 2023
2b809ec
Fix build issue
Siphonophora Apr 15, 2023
13ee09e
restore these items in verification
Siphonophora Apr 15, 2023
0d59af0
Fix order of regex causing test failures
Siphonophora Apr 15, 2023
86eba90
Add comments related to dev issues.
Siphonophora Apr 15, 2023
bde3db7
Update equals and hash code
Siphonophora Apr 15, 2023
ef3bfac
Pre-release of 8 was causing build errors.
Siphonophora Apr 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/TestLogger/Core/IInputSanitizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Spekt Contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Spekt.TestLogger.Core
{
public interface IInputSanitizer
{
string Sanitize(string input);
}
}
2 changes: 2 additions & 0 deletions src/TestLogger/Core/ITestResultSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ namespace Spekt.TestLogger.Core

public interface ITestResultSerializer
{
IInputSanitizer InputSanitizer { get; }

string Serialize(
LoggerConfiguration loggerConfiguration,
TestRunConfiguration runConfiguration,
Expand Down
35 changes: 35 additions & 0 deletions src/TestLogger/Core/InputSanitizerXml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Spekt Contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Spekt.TestLogger.Core
{
using System.Text.RegularExpressions;

public class InputSanitizerXml : IInputSanitizer
{
private static readonly Regex InvalidXmlChar = new (@"([^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD]|[\u007F-\u0084\u0086-\u009F\uFDD0-\uFDEF])", RegexOptions.Compiled);
codito marked this conversation as resolved.
Show resolved Hide resolved

public string Sanitize(string input)
{
if (input == null)
{
return null;
}

// From xml spec (http://www.w3.org/TR/xml/#charsets) valid chars:
// #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
// Following control charset are discouraged:
// [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDEF],
// We are handling only #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
// because C# support unicode character in range \u0000 to \uFFFF
var evaluator = new MatchEvaluator(ReplaceInvalidCharacterWithUniCodeEscapeSequence);
return InvalidXmlChar.Replace(input, evaluator);

static string ReplaceInvalidCharacterWithUniCodeEscapeSequence(Match match)
{
char x = match.Value[0];
return $@"\u{(ushort)x:x4}";
}
}
}
}
11 changes: 9 additions & 2 deletions src/TestLogger/Core/TestMessageInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@

namespace Spekt.TestLogger.Core
{
using System;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;

/// <summary>
/// A message generated during the test run.
/// </summary>
public class TestMessageInfo
{
public TestMessageLevel Level { get; set; }
public TestMessageInfo(TestMessageLevel level, string message)
{
this.Level = level;
this.Message = message ?? throw new ArgumentNullException(nameof(message));
}

public string Message { get; set; }
public TestMessageLevel Level { get; }

public string Message { get; }
}
}
141 changes: 105 additions & 36 deletions src/TestLogger/Core/TestResultInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,75 +4,144 @@
namespace Spekt.TestLogger.Core
{
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Spekt.TestLogger.Extensions;

public sealed class TestResultInfo
{
private readonly TestResult result;

public TestResultInfo(
TestResult result,
string @namespace,
string type,
string method)
string method,
TestOutcome outcome,
string testResultDisplayName,
string testCaseDisplayName,
string assemblyPath,
string codeFilePath,
int lineNumber,
DateTime startTime,
DateTime endTime,
TimeSpan duration,
string errorMessage,
string errorStackTrace,
List<TestResultMessage> messages,
IReadOnlyCollection<Trait> traits,
string executorUri)
{
this.result = result;
this.Namespace = @namespace;
this.Type = type;
this.Method = method;
this.Outcome = result.Outcome;
this.Outcome = outcome;
this.TestResultDisplayName = testResultDisplayName;
this.TestCaseDisplayName = testCaseDisplayName;
this.AssemblyPath = assemblyPath;
this.CodeFilePath = codeFilePath;
this.LineNumber = lineNumber;
this.StartTime = startTime;
this.EndTime = endTime;
this.Duration = duration;
this.ErrorMessage = errorMessage;
this.ErrorStackTrace = errorStackTrace;
this.Messages = messages;
this.Traits = traits;
this.ExecutorUri = executorUri;
}

public TestCase TestCase => this.result.TestCase;

public TestOutcome Outcome { get; set; }

public string AssemblyPath => this.result.TestCase.Source;
public string Namespace { get; }

public string Namespace { get; private set; }

public string Type { get; private set; }

public string FullTypeName => this.Namespace + "." + this.Type;
public string Type { get; }

/// <summary>
/// Gets a string that contain the method name, along with any paramaterized data related to
/// the method. For example, `SomeMethod` or `SomeParameterizedMethod(true)`.
/// </summary>
public string Method { get; internal set; }

public DateTime StartTime => this.result.StartTime.UtcDateTime;
public TestOutcome Outcome { get; set; }

public DateTime EndTime => this.result.EndTime.UtcDateTime;
public string AssemblyPath { get; }

public TimeSpan Duration => this.result.Duration;
public string CodeFilePath { get; }

public string ErrorMessage => this.result.ErrorMessage;
public int LineNumber { get; }

public string ErrorStackTrace => this.result.ErrorStackTrace;
public DateTime StartTime { get; }

public Collection<TestResultMessage> Messages => this.result.Messages;
public DateTime EndTime { get; }

public TraitCollection Traits => this.result.Traits;
public TimeSpan Duration { get; }

internal TestResult Result => this.result;
public string ErrorMessage { get; }

public override int GetHashCode()
{
return this.result.GetHashCode();
}
public string ErrorStackTrace { get; }

public List<TestResultMessage> Messages { get; }

public IReadOnlyCollection<Trait> Traits { get; }

public string ExecutorUri { get; }

public string FullTypeName => this.Namespace + "." + this.Type;

/// <summary>
/// Gets value that originated at <see cref="TestResult.DisplayName"/>. Intended for use within
/// this library by framework specific adapters, to ensure that <see cref="Method"/> has the
/// proper value.
/// </summary>
internal string TestResultDisplayName { get; }

/// <summary>
/// Gets value that originated at <see cref="TestCase.DisplayName"/>. Intended for use within
/// this library by framework specific adapters, to ensure that <see cref="Method"/> has the
/// proper value.
/// </summary>
internal string TestCaseDisplayName { get; }

public override bool Equals(object obj)
{
if (obj is not TestResultInfo objectToCompare)
{
return false;
}
return obj is TestResultInfo info &&
this.Namespace == info.Namespace &&
this.Type == info.Type &&
this.Method == info.Method &&
this.Outcome == info.Outcome &&
this.TestResultDisplayName == info.TestResultDisplayName &&
this.TestCaseDisplayName == info.TestCaseDisplayName &&
this.AssemblyPath == info.AssemblyPath &&
this.CodeFilePath == info.CodeFilePath &&
this.LineNumber == info.LineNumber &&
this.StartTime == info.StartTime &&
this.EndTime == info.EndTime &&
this.Duration.Equals(info.Duration) &&
this.ErrorMessage == info.ErrorMessage &&
codito marked this conversation as resolved.
Show resolved Hide resolved
this.ErrorStackTrace == info.ErrorStackTrace &&
EqualityComparer<List<TestResultMessage>>.Default.Equals(this.Messages, info.Messages) &&
EqualityComparer<IReadOnlyCollection<Trait>>.Default.Equals(this.Traits, info.Traits) &&
this.ExecutorUri == info.ExecutorUri &&
this.FullTypeName == info.FullTypeName;
}

return string.Compare(this.ErrorMessage, objectToCompare.ErrorMessage, StringComparison.CurrentCulture) == 0
&& string.Compare(this.ErrorStackTrace, objectToCompare.ErrorStackTrace, StringComparison.CurrentCulture) == 0;
public override int GetHashCode()
{
int hashCode = -1082088776;
hashCode = (hashCode * -1521134295) + EqualityComparer<string>.Default.GetHashCode(this.Namespace);
hashCode = (hashCode * -1521134295) + EqualityComparer<string>.Default.GetHashCode(this.Type);
hashCode = (hashCode * -1521134295) + EqualityComparer<string>.Default.GetHashCode(this.Method);
hashCode = (hashCode * -1521134295) + this.Outcome.GetHashCode();
hashCode = (hashCode * -1521134295) + EqualityComparer<string>.Default.GetHashCode(this.TestResultDisplayName);
hashCode = (hashCode * -1521134295) + EqualityComparer<string>.Default.GetHashCode(this.TestCaseDisplayName);
hashCode = (hashCode * -1521134295) + EqualityComparer<string>.Default.GetHashCode(this.AssemblyPath);
hashCode = (hashCode * -1521134295) + EqualityComparer<string>.Default.GetHashCode(this.CodeFilePath);
hashCode = (hashCode * -1521134295) + this.LineNumber.GetHashCode();
hashCode = (hashCode * -1521134295) + this.StartTime.GetHashCode();
hashCode = (hashCode * -1521134295) + this.EndTime.GetHashCode();
hashCode = (hashCode * -1521134295) + this.Duration.GetHashCode();
hashCode = (hashCode * -1521134295) + EqualityComparer<string>.Default.GetHashCode(this.ErrorMessage);
hashCode = (hashCode * -1521134295) + EqualityComparer<string>.Default.GetHashCode(this.ErrorStackTrace);
hashCode = (hashCode * -1521134295) + EqualityComparer<List<TestResultMessage>>.Default.GetHashCode(this.Messages);
hashCode = (hashCode * -1521134295) + EqualityComparer<IReadOnlyCollection<Trait>>.Default.GetHashCode(this.Traits);
hashCode = (hashCode * -1521134295) + EqualityComparer<string>.Default.GetHashCode(this.ExecutorUri);
hashCode = (hashCode * -1521134295) + EqualityComparer<string>.Default.GetHashCode(this.FullTypeName);
return hashCode;
}
}
}
3 changes: 1 addition & 2 deletions src/TestLogger/Core/TestRunCompleteWorkflow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ public static void Complete(this ITestRun testRun, TestRunCompleteEventArgs comp
var transformedResults = results;
if (transformedResults.Any())
{
var executorUri = transformedResults[0]
.TestCase.ExecutorUri?.ToString();
var executorUri = transformedResults[0].ExecutorUri;
var adapter = testRun.AdapterFactory.CreateTestAdapter(executorUri);
transformedResults = adapter.TransformResults(results, messages);
}
Expand Down
5 changes: 4 additions & 1 deletion src/TestLogger/Core/TestRunMessageWorkflow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ public static class TestRunMessageWorkflow
{
public static void Message(this ITestRun testRun, TestRunMessageEventArgs messageEvent)
{
testRun.Store.Add(new TestMessageInfo { Level = messageEvent.Level, Message = messageEvent.Message });
testRun.Store.Add(
new TestMessageInfo(
messageEvent.Level,
testRun.Serializer.InputSanitizer.Sanitize(messageEvent.Message)));
}
}
}
26 changes: 22 additions & 4 deletions src/TestLogger/Core/TestRunResultWorkflow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
namespace Spekt.TestLogger.Core
{
using System;
using System.Linq;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;

public static class TestRunResultWorkflow
Expand All @@ -22,11 +24,27 @@ string x when x.Equals("Legacy", StringComparison.OrdinalIgnoreCase) => LegacyPa
_ => Parser.Parse(fqn),
};

var result = resultEvent.Result;
Func<string, string> sanitize = testRun.Serializer.InputSanitizer.Sanitize;
codito marked this conversation as resolved.
Show resolved Hide resolved

testRun.Store.Add(new TestResultInfo(
resultEvent.Result,
parsedName.Namespace,
parsedName.Type,
parsedName.Method));
sanitize(parsedName.Namespace),
sanitize(parsedName.Type),
sanitize(parsedName.Method),
result.Outcome,
sanitize(result.DisplayName),
sanitize(result.TestCase.DisplayName),
sanitize(result.TestCase.Source),
sanitize(result.TestCase.CodeFilePath),
result.TestCase.LineNumber,
result.StartTime.UtcDateTime,
result.EndTime.UtcDateTime,
result.Duration,
sanitize(result.ErrorMessage),
sanitize(result.ErrorStackTrace),
result.Messages.Select(x => new TestResultMessage(sanitize(x.Category), sanitize(x.Text))).ToList(),
result.TestCase.Traits.Select(x => new Trait(sanitize(x.Name), sanitize(x.Value))).ToList(),
result.TestCase.ExecutorUri?.ToString()));
}
}
}
2 changes: 1 addition & 1 deletion src/TestLogger/Extensions/MSTestAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public List<TestResultInfo> TransformResults(List<TestResultInfo> results, List<
// So we use the DisplayName whenever it is available.
foreach (var result in results)
{
string displayName = result.Result.DisplayName;
string displayName = result.TestResultDisplayName;
string method = result.Method;

if (string.IsNullOrWhiteSpace(displayName))
Expand Down
2 changes: 1 addition & 1 deletion src/TestLogger/Extensions/NUnitTestAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public List<TestResultInfo> TransformResults(List<TestResultInfo> results, List<
// is passed as a trait in the test platform. NUnit explicit attribute spec:
// https://docs.nunit.org/articles/nunit/writing-tests/attributes/explicit.html
if (result.Outcome == Microsoft.VisualStudio.TestPlatform.ObjectModel.TestOutcome.None &&
result.TestCase.Traits.Any(trait => trait.Name.Equals(ExplicitLabel, StringComparison.OrdinalIgnoreCase)))
result.Traits.Any(trait => trait.Name.Equals(ExplicitLabel, StringComparison.OrdinalIgnoreCase)))
codito marked this conversation as resolved.
Show resolved Hide resolved
{
result.Outcome = Microsoft.VisualStudio.TestPlatform.ObjectModel.TestOutcome.Skipped;
}
Expand Down
4 changes: 2 additions & 2 deletions src/TestLogger/Extensions/XunitTestAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ public List<TestResultInfo> TransformResults(

foreach (var result in results)
{
if (skippedTestNamesWithReason.TryGetValue(result.Result.TestCase.DisplayName, out var skipReason))
if (skippedTestNamesWithReason.TryGetValue(result.TestCaseDisplayName, out var skipReason))
{
// TODO: Defining a new category for now...
result.Messages.Add(new TestResultMessage("skipReason", skipReason));
}

string displayName = result.Result.DisplayName;
string displayName = result.TestResultDisplayName;

// Add parameters for theories.
if (string.IsNullOrWhiteSpace(displayName) == false &&
Expand Down
24 changes: 0 additions & 24 deletions src/TestLogger/Utilities/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,6 @@ namespace Spekt.TestLogger.Utilities

public static class StringExtensions
{
public static string ReplaceInvalidXmlChar(this string str)
Siphonophora marked this conversation as resolved.
Show resolved Hide resolved
{
if (str != null)
{
// From xml spec (http://www.w3.org/TR/xml/#charsets) valid chars:
// #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
// Following control charset are discouraged:
// [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDEF],
// We are handling only #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
// because C# support unicode character in range \u0000 to \uFFFF
const string invalidChar = @"([^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD]|[\u007F-\u0084\u0086-\u009F\uFDD0-\uFDEF])";
MatchEvaluator evaluator = ReplaceInvalidCharacterWithUniCodeEscapeSequence;
return Regex.Replace(str, invalidChar, evaluator);
}

return str;
}

public static string SubstringAfterDot(this string name)
{
if (string.IsNullOrEmpty(name))
Expand Down Expand Up @@ -56,11 +38,5 @@ public static string SubstringBeforeDot(this string name)

return string.Empty;
}

private static string ReplaceInvalidCharacterWithUniCodeEscapeSequence(Match match)
{
char x = match.Value[0];
return string.Format(@"\u{0:x4}", (ushort)x);
}
}
}
Loading